diff --git a/Api/Data/PaymentLinkRedirectResultInterface.php b/Api/Data/PaymentLinkRedirectResultInterface.php
new file mode 100644
index 00000000000..560aab710d5
--- /dev/null
+++ b/Api/Data/PaymentLinkRedirectResultInterface.php
@@ -0,0 +1,20 @@
+setReadonly(true, true);
+ return parent::_getElementHtml($element);
+ }
+}
diff --git a/Block/Form/Pointofsale.php b/Block/Form/Pointofsale.php
index 7f197ad22e8..c2b751f13aa 100644
--- a/Block/Form/Pointofsale.php
+++ b/Block/Form/Pointofsale.php
@@ -8,9 +8,7 @@
use Magento\Framework\View\Element\Template\Context;
use Magento\Payment\Block\Form;
-use Mollie\Api\Exceptions\ApiException;
-use Mollie\Api\Resources\Terminal;
-use Mollie\Payment\Service\Mollie\MollieApiClient;
+use Mollie\Payment\Service\Mollie\AvailableTerminals;
/**
* Class Pointofsale
@@ -20,22 +18,23 @@
class Pointofsale extends Form
{
/**
- * @var string
+ * @var AvailableTerminals
*/
- protected $_template = 'Mollie_Payment::form/pointofsale.phtml';
+ private $availableTerminals;
+
/**
- * @var MollieApiClient
+ * @var string
*/
- private $mollieApiClient;
+ protected $_template = 'Mollie_Payment::form/pointofsale.phtml';
public function __construct(
Context $context,
- MollieApiClient $mollieApiClient,
+ AvailableTerminals $availableTerminals,
array $data = []
) {
parent::__construct($context, $data);
- $this->mollieApiClient = $mollieApiClient;
+ $this->availableTerminals = $availableTerminals;
}
/**
@@ -46,36 +45,11 @@ public function __construct(
* serialNumber: string|null,
* description: string
* }
- * @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getTerminals(): array
{
$storeId = $this->_storeManager->getStore()->getId();
- try {
- $mollieApiClient = $this->mollieApiClient->loadByStore((int)$storeId);
- $terminals = $mollieApiClient->terminals->page();
- } catch (ApiException $exception) {
- return [];
- }
-
- $output = [];
- /** @var Terminal $terminal */
- foreach ($terminals as $terminal) {
- if (!$terminal->isActive()) {
- continue;
- }
-
- $output[] = [
- 'id' => $terminal->id,
- 'brand' => $terminal->brand,
- 'model' => $terminal->model,
- 'serialNumber' => $terminal->serialNumber,
- 'description' => $terminal->description,
- ];
- }
-
- return $output;
+ return $this->availableTerminals->execute((int)$storeId);
}
}
diff --git a/Config.php b/Config.php
index 3a7292fc2e8..6613d427cfe 100644
--- a/Config.php
+++ b/Config.php
@@ -31,6 +31,7 @@ class Config
const GENERAL_DASHBOARD_URL_PAYMENTS_API = 'payment/mollie_general/dashboard_url_payments_api';
const GENERAL_ENABLE_MAGENTO_VAULT = 'payment/mollie_general/enable_magento_vault';
const GENERAL_ENABLE_SECOND_CHANCE_EMAIL = 'payment/mollie_general/enable_second_chance_email';
+ const GENERAL_PROCESS_TRANSACTION_IN_THE_QUEUE = 'payment/mollie_general/process_transactions_in_the_queue';
const GENERAL_ENCRYPT_PAYMENT_DETAILS = 'payment/mollie_general/encrypt_payment_details';
const GENERAL_INCLUDE_SHIPPING_IN_SURCHARGE = 'payment/mollie_general/include_shipping_in_surcharge';
const GENERAL_INVOICE_NOTIFY = 'payment/mollie_general/invoice_notify';
@@ -209,20 +210,35 @@ public function getApiKey($storeId = null)
}
if (!$this->isProductionMode($storeId)) {
- $apiKey = trim($this->getPath(static::GENERAL_APIKEY_TEST, $storeId) ?? '');
- if (empty($apiKey)) {
- $this->addToLog('error', 'Mollie API key not set (test modus)');
- }
-
- if (!preg_match('/^test_\w+$/', $apiKey)) {
- $this->addToLog('error', 'Mollie set to test modus, but API key does not start with "test_"');
- }
+ $apiKey = $this->getTestApiKey($storeId === null ? null : (int)$storeId);
$keys[$storeId] = $apiKey;
return $apiKey;
}
- $apiKey = trim($this->getPath(static::GENERAL_APIKEY_LIVE, $storeId) ?? '');
+ $apiKey = $this->getLiveApiKey($storeId === null ? null : (int)$storeId);
+
+ $keys[$storeId] = $apiKey;
+ return $apiKey;
+ }
+
+ public function getTestApiKey(int $storeId = null): string
+ {
+ $apiKey = trim((string)$this->getPath(static::GENERAL_APIKEY_TEST, $storeId) ?? '');
+ if (empty($apiKey)) {
+ $this->addToLog('error', 'Mollie API key not set (test modus)');
+ }
+
+ if (!preg_match('/^test_\w+$/', $apiKey)) {
+ $this->addToLog('error', 'Mollie set to test modus, but API key does not start with "test_"');
+ }
+
+ return $apiKey;
+ }
+
+ public function getLiveApiKey(int $storeId = null): string
+ {
+ $apiKey = trim((string)$this->getPath(static::GENERAL_APIKEY_LIVE, $storeId) ?? '');
if (empty($apiKey)) {
$this->addToLog('error', 'Mollie API key not set (live modus)');
}
@@ -231,7 +247,6 @@ public function getApiKey($storeId = null)
$this->addToLog('error', 'Mollie set to live modus, but API key does not start with "live_"');
}
- $keys[$storeId] = $apiKey;
return $apiKey;
}
@@ -756,6 +771,11 @@ public function isMultishippingEnabled(): bool
return $this->moduleManager->isEnabled('Mollie_Multishipping');
}
+ public function processTransactionsInTheQueue(int $storeId = null): bool
+ {
+ return $this->isSetFlag(static::GENERAL_PROCESS_TRANSACTION_IN_THE_QUEUE, $storeId);
+ }
+
public function encryptPaymentDetails($storeId = null): bool
{
return $this->isSetFlag(static::GENERAL_ENCRYPT_PAYMENT_DETAILS, $storeId);
diff --git a/Controller/Checkout/Process.php b/Controller/Checkout/Process.php
index 89e17e8fabc..ed3e0cc45f3 100644
--- a/Controller/Checkout/Process.php
+++ b/Controller/Checkout/Process.php
@@ -17,6 +17,9 @@
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Checkout\Model\Session;
+use Mollie\Payment\Service\Mollie\GetMollieStatusResult;
+use Mollie\Payment\Service\Mollie\Order\SuccessPageRedirect;
+use Mollie\Payment\Service\Mollie\ProcessTransaction;
use Mollie\Payment\Service\Mollie\ValidateProcessRequest;
use Mollie\Payment\Service\Order\RedirectOnError;
@@ -63,6 +66,14 @@ class Process extends Action
* @var ValidateProcessRequest
*/
private $validateProcessRequest;
+ /**
+ * @var ProcessTransaction
+ */
+ private $processTransaction;
+ /**
+ * @var SuccessPageRedirect
+ */
+ private $successPageRedirect;
public function __construct(
Context $context,
@@ -73,7 +84,9 @@ public function __construct(
OrderRepositoryInterface $orderRepository,
RedirectOnError $redirectOnError,
ManagerInterface $eventManager,
- ValidateProcessRequest $validateProcessRequest
+ ValidateProcessRequest $validateProcessRequest,
+ ProcessTransaction $processTransaction,
+ SuccessPageRedirect $successPageRedirect
) {
$this->checkoutSession = $checkoutSession;
$this->paymentHelper = $paymentHelper;
@@ -83,6 +96,8 @@ public function __construct(
$this->redirectOnError = $redirectOnError;
$this->eventManager = $eventManager;
$this->validateProcessRequest = $validateProcessRequest;
+ $this->processTransaction = $processTransaction;
+ $this->successPageRedirect = $successPageRedirect;
parent::__construct($context);
}
@@ -100,9 +115,10 @@ public function execute()
}
try {
- $result = [];
+ $result = null;
foreach ($orderIds as $orderId => $paymentToken) {
- $result = $this->mollieModel->processTransaction($orderId, 'success', $paymentToken);
+ $order = $this->orderRepository->get($orderId);
+ $result = $this->processTransaction->execute($orderId, $order->getMollieTransactionId());
}
} catch (\Exception $e) {
$this->mollieHelper->addTolog('error', $e->getMessage());
@@ -110,26 +126,10 @@ public function execute()
return $this->_redirect($this->redirectOnError->getUrl());
}
- if (!empty($result['success'])) {
+ if ($result !== null && in_array($result->getStatus(), ['paid', 'authorized'])) {
try {
- $this->checkoutSession->start();
-
- $redirect = new DataObject([
- 'path' => 'checkout/onepage/success',
- 'query' => ['utm_nooverride' => 1],
- ]);
-
- $this->eventManager->dispatch('mollie_checkout_success_redirect', [
- 'redirect' => $redirect,
- 'order_ids' => $orderIds,
- 'request' => $this->getRequest(),
- 'response' => $this->getResponse(),
- ]);
-
- return $this->_redirect($redirect->getData('path'), [
- '_query' => $redirect->getData('query'),
- '_use_rewrite' => false,
- ]);
+ $this->successPageRedirect->execute($order, $orderIds);
+ return $this->getResponse();
} catch (\Exception $e) {
$this->mollieHelper->addTolog('error', $e->getMessage());
$this->messageManager->addErrorMessage(__('Transaction failed. Please verify your billing information and payment method, and try again.'));
@@ -140,7 +140,7 @@ public function execute()
return $this->handleNonSuccessResult($result, $orderIds);
}
- protected function handleNonSuccessResult(array $result, array $orderIds): ResponseInterface
+ protected function handleNonSuccessResult(GetMollieStatusResult $result, array $orderIds): ResponseInterface
{
$this->checkIfLastRealOrder($orderIds);
$this->checkoutSession->restoreQuote();
@@ -149,23 +149,20 @@ protected function handleNonSuccessResult(array $result, array $orderIds): Respo
return $this->_redirect($this->redirectOnError->getUrl());
}
- /**
- * @param array $result
- */
- protected function addResultMessage(array $result)
+ protected function addResultMessage(GetMollieStatusResult $result)
{
- if (!isset($result['status'])) {
- $this->messageManager->addErrorMessage(__('Transaction failed. Please verify your billing information and payment method, and try again.'));
- return;
- }
-
- if ($result['status'] == 'canceled') {
+ if ($result->getStatus() == 'canceled') {
$this->messageManager->addNoticeMessage(__('Payment canceled, please try again.'));
return;
}
- if ($result['status'] == 'failed' && isset($result['method'])) {
- $this->messageManager->addErrorMessage(__('Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment.', $result['method']));
+ if ($result->getStatus() == 'failed' && $result->getMethod()) {
+ $this->messageManager->addErrorMessage(
+ __(
+ 'Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment.',
+ $result->getMethod()
+ )
+ );
return;
}
diff --git a/Controller/Checkout/Webhook.php b/Controller/Checkout/Webhook.php
index d603ec9a61f..d7d5e628f18 100644
--- a/Controller/Checkout/Webhook.php
+++ b/Controller/Checkout/Webhook.php
@@ -16,6 +16,7 @@
use Magento\Sales\Api\OrderRepositoryInterface;
use Mollie\Payment\Helper\General as MollieHelper;
use Mollie\Payment\Model\Mollie as MollieModel;
+use Mollie\Payment\Service\Mollie\ProcessTransaction;
use Mollie\Payment\Service\OrderLockService;
/**
@@ -55,6 +56,10 @@ class Webhook extends Action
* @var OrderLockService
*/
private $orderLockService;
+ /**
+ * @var ProcessTransaction
+ */
+ private $processTransaction;
public function __construct(
Context $context,
@@ -63,7 +68,8 @@ public function __construct(
MollieHelper $mollieHelper,
OrderRepositoryInterface $orderRepository,
EncryptorInterface $encryptor,
- OrderLockService $orderLockService
+ OrderLockService $orderLockService,
+ ProcessTransaction $processTransaction
) {
$this->checkoutSession = $checkoutSession;
$this->resultFactory = $context->getResultFactory();
@@ -72,6 +78,7 @@ public function __construct(
$this->orderRepository = $orderRepository;
$this->encryptor = $encryptor;
$this->orderLockService = $orderLockService;
+ $this->processTransaction = $processTransaction;
parent::__construct($context);
}
@@ -103,8 +110,7 @@ public function execute()
throw new \Exception('Order is locked, skipping webhook', 425);
}
- $order->setMollieTransactionId($transactionId);
- $this->mollieModel->processTransactionForOrder($order, 'webhook');
+ $this->processTransaction->execute((int)$order->getEntityId(), $transactionId);
}
return $this->getOkResponse();
diff --git a/GraphQL/Resolver/Cart/AvailableIssuersForMethod.php b/GraphQL/Resolver/Cart/AvailableIssuersForMethod.php
index 1058334eb25..261137b3db2 100644
--- a/GraphQL/Resolver/Cart/AvailableIssuersForMethod.php
+++ b/GraphQL/Resolver/Cart/AvailableIssuersForMethod.php
@@ -6,8 +6,6 @@
namespace Mollie\Payment\GraphQL\Resolver\Cart;
-use Mollie\Payment\Helper\General;
-use Mollie\Payment\Model\Mollie;
use Mollie\Payment\Service\Mollie\GetIssuers;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -15,28 +13,14 @@
class AvailableIssuersForMethod implements ResolverInterface
{
- /**
- * @var Mollie
- */
- private $mollieModel;
-
- /**
- * @var General
- */
- private $mollieHelper;
-
/**
* @var GetIssuers
*/
private $getIssuers;
public function __construct(
- Mollie $mollieModel,
- General $mollieHelper,
GetIssuers $getIssuers
) {
- $this->mollieModel = $mollieModel;
- $this->mollieHelper = $mollieHelper;
$this->getIssuers = $getIssuers;
}
diff --git a/GraphQL/Resolver/Cart/AvailableTerminalsForMethod.php b/GraphQL/Resolver/Cart/AvailableTerminalsForMethod.php
new file mode 100644
index 00000000000..23f950a6ec2
--- /dev/null
+++ b/GraphQL/Resolver/Cart/AvailableTerminalsForMethod.php
@@ -0,0 +1,51 @@
+pointOfSaleAvailability = $pointOfSaleAvailability;
+ $this->availableTerminals = $availableTerminals;
+ }
+
+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
+ {
+ $method = $value['code'];
+ if ($method != 'mollie_methods_pointofsale' || !$context->getExtensionAttributes()->getIsCustomer()) {
+ return [];
+ }
+
+ $storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
+ $customerGroupId = $context->getExtensionAttributes()->getCustomerGroupId();
+ if (!$this->pointOfSaleAvailability->isAvailableForCustomerGroupId($customerGroupId, $storeId)) {
+ return [];
+ }
+
+ return $this->availableTerminals->execute((int)$storeId);
+ }
+}
diff --git a/GraphQL/Resolver/Checkout/ProcessTransaction.php b/GraphQL/Resolver/Checkout/ProcessTransaction.php
index 7e55de40d85..7e53b34e233 100644
--- a/GraphQL/Resolver/Checkout/ProcessTransaction.php
+++ b/GraphQL/Resolver/Checkout/ProcessTransaction.php
@@ -13,18 +13,12 @@
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Quote\Api\CartRepositoryInterface;
-use Mollie\Api\Types\PaymentStatus;
+use Magento\Sales\Api\OrderRepositoryInterface;
use Mollie\Payment\Api\PaymentTokenRepositoryInterface;
-use Mollie\Payment\Model\Mollie;
-use Mollie\Payment\Service\Mollie\ShouldRedirectToSuccessPage;
+use Mollie\Payment\Service\Mollie\ProcessTransaction as ProcessTransactionAction;
class ProcessTransaction implements ResolverInterface
{
- /**
- * @var Mollie
- */
- private $mollie;
-
/**
* @var PaymentTokenRepositoryInterface
*/
@@ -36,20 +30,24 @@ class ProcessTransaction implements ResolverInterface
private $cartRepository;
/**
- * @var ShouldRedirectToSuccessPage
+ * @var ProcessTransactionAction
+ */
+ private $processTransaction;
+ /**
+ * @var OrderRepositoryInterface
*/
- private $shouldRedirectToSuccessPage;
+ private $orderRepository;
public function __construct(
- Mollie $mollie,
PaymentTokenRepositoryInterface $paymentTokenRepository,
CartRepositoryInterface $cartRepository,
- ShouldRedirectToSuccessPage $shouldRedirectToSuccessPage
+ ProcessTransactionAction $processTransaction,
+ OrderRepositoryInterface $orderRepository
) {
- $this->mollie = $mollie;
$this->paymentTokenRepository = $paymentTokenRepository;
$this->cartRepository = $cartRepository;
- $this->shouldRedirectToSuccessPage = $shouldRedirectToSuccessPage;
+ $this->processTransaction = $processTransaction;
+ $this->orderRepository = $orderRepository;
}
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
@@ -65,8 +63,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
throw new GraphQlNoSuchEntityException(__('No order found with token "%1"', $token));
}
- $result = $this->mollie->processTransaction($tokenModel->getOrderId(), 'success', $token);
- $redirectToSuccessPage = $this->shouldRedirectToSuccessPage->execute($result);
+ $order = $this->orderRepository->get($tokenModel->getOrderId());
+ $result = $this->processTransaction->execute($tokenModel->getOrderId(), $order->getMollieTransactionId());
+ $redirectToSuccessPage = in_array($result->getStatus(), ['pending', 'paid', 'authorized']);
$cart = null;
if ($tokenModel->getCartId()) {
@@ -74,7 +73,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
}
return [
- 'paymentStatus' => strtoupper($result['status']),
+ 'paymentStatus' => strtoupper($result->getStatus()),
'cart' => $cart,
'redirect_to_cart' => !$redirectToSuccessPage,
'redirect_to_success_page' => $redirectToSuccessPage,
diff --git a/Model/Adminhtml/Backend/ChangeApiMode.php b/Model/Adminhtml/Backend/ChangeApiMode.php
new file mode 100644
index 00000000000..7e632bf6229
--- /dev/null
+++ b/Model/Adminhtml/Backend/ChangeApiMode.php
@@ -0,0 +1,85 @@
+mollieConfig = $mollieConfig;
+ $this->flushMollieCache = $flushMollieCache;
+ $this->updateProfileId = $updateProfileId;
+ }
+
+ public function beforeSave(): self
+ {
+ $this->flushMollieCache->flush();
+
+ return parent::beforeSave();
+ }
+
+ public function afterSave()
+ {
+ $apiKey = $this->getApiKey($this->getValue());
+ $this->updateProfileId->execute($apiKey, $this->getScope(), $this->getScopeId());
+
+ return parent::afterSave();
+ }
+
+ private function getApiKey(string $mode): string
+ {
+ if ($mode === 'live') {
+ return $this->mollieConfig->getLiveApiKey((int)$this->getScopeId());
+ }
+
+ return $this->mollieConfig->getTestApiKey((int)$this->getScopeId());
+ }
+}
diff --git a/Model/Adminhtml/Backend/DoNoUpdate.php b/Model/Adminhtml/Backend/DoNoUpdate.php
new file mode 100644
index 00000000000..e81fe95986f
--- /dev/null
+++ b/Model/Adminhtml/Backend/DoNoUpdate.php
@@ -0,0 +1,19 @@
+getOldValue() != $this->getValue()) {
- $this->_cacheManager->clean(['mollie_payment', 'mollie_payment_methods']);
+ $this->flush();
}
return $this;
}
+
+ public function flush(): void
+ {
+ $this->_cacheManager->clean(['mollie_payment', 'mollie_payment_methods']);
+ }
}
diff --git a/Model/Adminhtml/Backend/SaveApiKey.php b/Model/Adminhtml/Backend/SaveApiKey.php
index 6715f0d7a6c..7821e1f381a 100644
--- a/Model/Adminhtml/Backend/SaveApiKey.php
+++ b/Model/Adminhtml/Backend/SaveApiKey.php
@@ -26,6 +26,15 @@ class SaveApiKey extends Encrypted
* @var ApiKeyFallbackInterfaceFactory
*/
private $apiKeyFallbackFactory;
+ /**
+ * @var UpdateProfileId
+ */
+ private $updateProfileId;
+
+ /**
+ * @var bool|string
+ */
+ private $shouldUpdateProfileId = false;
public function __construct(
Context $context,
@@ -35,6 +44,7 @@ public function __construct(
EncryptorInterface $encryptor,
ApiKeyFallbackRepositoryInterface $apiKeyFallbackRepository,
ApiKeyFallbackInterfaceFactory $apiKeyFallbackFactory,
+ UpdateProfileId $updateProfileId,
AbstractResource $resource = null,
AbstractDb $resourceCollection = null,
array $data = []
@@ -52,6 +62,7 @@ public function __construct(
$this->apiKeyFallbackRepository = $apiKeyFallbackRepository;
$this->apiKeyFallbackFactory = $apiKeyFallbackFactory;
+ $this->updateProfileId = $updateProfileId;
}
public function beforeSave()
@@ -65,6 +76,8 @@ public function beforeSave()
// Validate the new API key before saving.
(new MollieApiClient())->setApiKey($value);
+ $this->shouldUpdateProfileId = $value;
+
$this->saveApiKey();
$this->_cacheManager->clean(['mollie_payment', 'mollie_payment_methods']);
}
@@ -72,6 +85,15 @@ public function beforeSave()
return $this;
}
+ public function afterSave()
+ {
+ if ($this->shouldUpdateProfileId !== false) {
+ $this->updateProfileId->execute($this->shouldUpdateProfileId, $this->getScope(), $this->getScopeId());
+ }
+
+ return parent::afterSave();
+ }
+
private function saveApiKey(): void
{
/** @var ApiKeyFallbackInterface $model */
diff --git a/Model/Adminhtml/Backend/UpdateProfileId.php b/Model/Adminhtml/Backend/UpdateProfileId.php
new file mode 100644
index 00000000000..1513ce9d456
--- /dev/null
+++ b/Model/Adminhtml/Backend/UpdateProfileId.php
@@ -0,0 +1,47 @@
+mollieApiClient = $mollieApiClient;
+ $this->configWriter = $configWriter;
+ }
+
+ public function execute(string $apiKey, string $scope, int $scopeId): void
+ {
+ $client = $this->mollieApiClient->loadByApiKey($apiKey);
+ $profile = $client->profiles->get('me');
+ $profileId = $profile->id;
+
+ $this->configWriter->save(
+ Config::GENERAL_PROFILEID,
+ $profileId,
+ $scope,
+ $scopeId
+ );
+ }
+}
diff --git a/Model/Api.php b/Model/Api.php
index 544a9a7c240..3fa398f939d 100644
--- a/Model/Api.php
+++ b/Model/Api.php
@@ -6,6 +6,7 @@
namespace Mollie\Payment\Model;
+use Magento\Framework\Module\Manager;
use Mollie\Payment\Config;
use Mollie\Payment\Helper\General as MollieHelper;
use Mollie\Api\MollieApiClient;
@@ -21,13 +22,19 @@ class Api extends MollieApiClient
* @var
*/
public $mollieHelper;
+ /**
+ * @var Manager
+ */
+ private $moduleManager;
public function __construct(
Config $config,
- MollieHelper $mollieHelper
+ MollieHelper $mollieHelper,
+ Manager $moduleManager
) {
$this->config = $config;
$this->mollieHelper = $mollieHelper;
+ $this->moduleManager = $moduleManager;
parent::__construct();
}
@@ -41,5 +48,13 @@ public function load($storeId = null)
$this->addVersionString('Magento/' . $this->config->getMagentoVersion());
$this->addVersionString('MagentoEdition/' . $this->config->getMagentoEdition());
$this->addVersionString('MollieMagento2/' . $this->mollieHelper->getExtensionVersion());
+
+ if ($this->moduleManager->isEnabled('Hyva_Theme')) {
+ $this->addVersionString('HyvaTheme');
+ }
+
+ if ($this->moduleManager->isEnabled('Hyva_Checkout')) {
+ $this->addVersionString('HyvaCheckout');
+ }
}
}
diff --git a/Model/OrderLines.php b/Model/OrderLines.php
index 5e98a29fe55..f1eaf9ef20d 100644
--- a/Model/OrderLines.php
+++ b/Model/OrderLines.php
@@ -16,7 +16,6 @@
use Magento\Sales\Api\Data\CreditmemoItemInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\ShipmentInterface;
-use Magento\Sales\Api\Data\ShipmentItemInterface;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\ResourceModel\Order\Handler\State;
use Mollie\Payment\Helper\General as MollieHelper;
@@ -219,7 +218,10 @@ public function getShipmentOrderLines(ShipmentInterface $shipment): array
if ($orderHasDiscount) {
$orderItem = $item->getOrderItem();
- $rowTotal = $orderItem->getBaseRowTotalInclTax() - $orderItem->getBaseDiscountAmount();
+ $rowTotal = $orderItem->getBaseRowTotal()
+ - $orderItem->getBaseDiscountAmount()
+ + $orderItem->getBaseTaxAmount()
+ + $orderItem->getBaseDiscountTaxCompensationAmount();
$line['amount'] = $this->mollieHelper->getAmountArray(
$order->getBaseCurrencyCode(),
@@ -288,10 +290,12 @@ public function getCreditmemoOrderLines(CreditmemoInterface $creditmemo, bool $a
];
if ($item->getBaseDiscountAmount()) {
- $line['amount'] = $this->mollieHelper->getAmountArray(
- $creditmemo->getBaseCurrencyCode(),
- $item->getBaseRowTotalInclTax() - $item->getBaseDiscountAmount()
- );
+ $rowTotal = $item->getBaseRowTotal()
+ - $item->getBaseDiscountAmount()
+ + $item->getBaseTaxAmount()
+ + $item->getBaseDiscountTaxCompensationAmount();
+
+ $line['amount'] = $this->mollieHelper->getAmountArray($creditmemo->getBaseCurrencyCode(), $rowTotal);
}
$orderLines[] = $line;
diff --git a/Model/Queue/TransactionToProcess.php b/Model/Queue/TransactionToProcess.php
new file mode 100644
index 00000000000..a6537423ad4
--- /dev/null
+++ b/Model/Queue/TransactionToProcess.php
@@ -0,0 +1,48 @@
+transactionId = $id;
+
+ return $this;
+ }
+
+ public function getTransactionId(): ?string
+ {
+ return $this->transactionId;
+ }
+
+ public function setOrderId(int $id): TransactionToProcessInterface
+ {
+ $this->orderId = $id;
+
+ return $this;
+ }
+
+ public function getOrderId(): ?int
+ {
+ return $this->orderId;
+ }
+}
diff --git a/Model/TransactionToOrder.php b/Model/TransactionToOrder.php
index 2c61ccbed6d..b59ae0bf638 100644
--- a/Model/TransactionToOrder.php
+++ b/Model/TransactionToOrder.php
@@ -92,6 +92,25 @@ public function setSkipped(int $skipped): TransactionToOrderInterface
return $this->setData(self::SKIPPED, $skipped);
}
+ /**
+ * Get redirected
+ * @return int|null
+ */
+ public function getRedirected(): ?int
+ {
+ return (int)$this->getData(self::REDIRECTED);
+ }
+
+ /**
+ * Set redirected
+ * @param int $redirected
+ * @return TransactionToOrderInterface
+ */
+ public function setRedirected(int $redirected): TransactionToOrderInterface
+ {
+ return $this->setData(self::REDIRECTED, $redirected);
+ }
+
/**
* Retrieve existing extension attributes object or create a new one.
* @return TransactionToOrderExtensionInterface|null
diff --git a/Queue/Handler/TransactionProcessor.php b/Queue/Handler/TransactionProcessor.php
new file mode 100644
index 00000000000..91430a1fea0
--- /dev/null
+++ b/Queue/Handler/TransactionProcessor.php
@@ -0,0 +1,59 @@
+orderRepository = $orderRepository;
+ $this->config = $config;
+ $this->mollieModel = $mollieModel;
+ }
+
+ public function execute(TransactionToProcessInterface $data): void
+ {
+ try {
+ $order = $this->orderRepository->get($data->getOrderId());
+ $order->setMollieTransactionId($data->getTransactionId());
+
+ $this->mollieModel->processTransactionForOrder($order, 'webhook');
+ } catch (\Throwable $throwable) {
+ $this->config->addToLog('error', [
+ 'from' => 'TransactionProcessor consumer',
+ 'message' => $throwable->getMessage(),
+ 'trace' => $throwable->getTraceAsString(),
+ 'order_id' => $data->getOrderId(),
+ 'transaction_id' => $data->getTransactionId(),
+ ]);
+ throw $throwable;
+ }
+ }
+}
diff --git a/Queue/Publisher/PublishTransactionToProcess.php b/Queue/Publisher/PublishTransactionToProcess.php
new file mode 100644
index 00000000000..43feb17a668
--- /dev/null
+++ b/Queue/Publisher/PublishTransactionToProcess.php
@@ -0,0 +1,39 @@
+publisher = $publisher;
+ }
+
+ public function publish(TransactionToProcessInterface $data): void
+ {
+ $this->publisher->publish(self::TOPIC_NAME, $data);
+ }
+}
diff --git a/Service/Magento/PaymentLinkRedirectResult.php b/Service/Magento/PaymentLinkRedirectResult.php
index 4c0c0c0566e..d51d62f6595 100644
--- a/Service/Magento/PaymentLinkRedirectResult.php
+++ b/Service/Magento/PaymentLinkRedirectResult.php
@@ -8,7 +8,9 @@
namespace Mollie\Payment\Service\Magento;
-class PaymentLinkRedirectResult
+use Mollie\Payment\Api\Data\PaymentLinkRedirectResultInterface;
+
+class PaymentLinkRedirectResult implements PaymentLinkRedirectResultInterface
{
/**
* @var bool
diff --git a/Service/Mollie/AvailableTerminals.php b/Service/Mollie/AvailableTerminals.php
new file mode 100644
index 00000000000..5dd8b9dd67f
--- /dev/null
+++ b/Service/Mollie/AvailableTerminals.php
@@ -0,0 +1,63 @@
+mollieApiClient = $mollieApiClient;
+ }
+
+ /**
+ * @return array{
+ * id: string,
+ * brand: string,
+ * model: string,
+ * serialNumber: string|null,
+ * description: string
+ * }
+ */
+ public function execute(int $storeId = null): array
+ {
+ try {
+ $mollieApiClient = $this->mollieApiClient->loadByStore($storeId);
+ $terminals = $mollieApiClient->terminals->page();
+ } catch (ApiException $exception) {
+ return [];
+ }
+
+ $output = [];
+ /** @var Terminal $terminal */
+ foreach ($terminals as $terminal) {
+ if (!$terminal->isActive()) {
+ continue;
+ }
+
+ $output[] = [
+ 'id' => $terminal->id,
+ 'brand' => $terminal->brand,
+ 'model' => $terminal->model,
+ 'serialNumber' => $terminal->serialNumber,
+ 'description' => $terminal->description,
+ ];
+ }
+
+ return $output;
+ }
+}
diff --git a/Service/Mollie/GetMollieStatus.php b/Service/Mollie/GetMollieStatus.php
new file mode 100644
index 00000000000..0692d7a6d46
--- /dev/null
+++ b/Service/Mollie/GetMollieStatus.php
@@ -0,0 +1,59 @@
+orderRepository = $orderRepository;
+ $this->mollieApiClient = $mollieApiClient;
+ $this->getMollieStatusResultFactory = $getMollieStatusResultFactory;
+ }
+
+ public function execute(int $orderId): GetMollieStatusResult
+ {
+ $order = $this->orderRepository->get($orderId);
+ $transactionId = $order->getMollieTransactionId();
+ $mollieApi = $this->mollieApiClient->loadByStore((int)$order->getStoreId());
+
+ if (substr($transactionId, 0, 4) == 'ord_') {
+ $mollieOrder = $mollieApi->orders->get($transactionId);
+
+ return $this->getMollieStatusResultFactory->create([
+ 'status' => $mollieOrder->status,
+ 'method' => $mollieOrder->method,
+ ]);
+ }
+
+ $molliePayment = $mollieApi->payments->get($transactionId);
+ return $this->getMollieStatusResultFactory->create([
+ 'status' => $molliePayment->status,
+ 'method' => $molliePayment->method,
+ ]);
+ }
+}
diff --git a/Service/Mollie/GetMollieStatusResult.php b/Service/Mollie/GetMollieStatusResult.php
new file mode 100644
index 00000000000..66edab8236b
--- /dev/null
+++ b/Service/Mollie/GetMollieStatusResult.php
@@ -0,0 +1,39 @@
+status = $status;
+ $this->method = $method;
+ }
+
+ public function getStatus(): string
+ {
+ return $this->status;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->method;
+ }
+}
diff --git a/Service/Mollie/MollieApiClient.php b/Service/Mollie/MollieApiClient.php
index af72df4748b..0c6730b9525 100644
--- a/Service/Mollie/MollieApiClient.php
+++ b/Service/Mollie/MollieApiClient.php
@@ -7,6 +7,7 @@
namespace Mollie\Payment\Service\Mollie;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Module\Manager;
use Mollie\Payment\Config;
use Mollie\Payment\Service\Mollie\Wrapper\FetchFallbackApiKeys;
use Mollie\Payment\Service\Mollie\Wrapper\MollieApiClientFallbackWrapper;
@@ -33,15 +34,21 @@ class MollieApiClient
* @var FetchFallbackApiKeys
*/
private $fetchFallbackApiKeys;
+ /**
+ * @var Manager
+ */
+ private $moduleManager;
public function __construct(
Config $config,
MollieApiClientFallbackWrapperFactory $mollieApiClientWrapperFactory,
- FetchFallbackApiKeys $fetchFallbackApiKeys
+ FetchFallbackApiKeys $fetchFallbackApiKeys,
+ Manager $moduleManager
) {
$this->config = $config;
$this->mollieApiClientWrapperFactory = $mollieApiClientWrapperFactory;
$this->fetchFallbackApiKeys = $fetchFallbackApiKeys;
+ $this->moduleManager = $moduleManager;
}
public function loadByStore(int $storeId = null): \Mollie\Api\MollieApiClient
@@ -68,6 +75,15 @@ public function loadByApiKey(string $apiKey): \Mollie\Api\MollieApiClient
$mollieApiClient->addVersionString('Magento/' . $this->config->getMagentoVersion());
$mollieApiClient->addVersionString('MagentoEdition/' . $this->config->getMagentoEdition());
$mollieApiClient->addVersionString('MollieMagento2/' . $this->config->getVersion());
+
+ if ($this->moduleManager->isEnabled('Hyva_Theme')) {
+ $mollieApiClient->addVersionString('HyvaTheme');
+ }
+
+ if ($this->moduleManager->isEnabled('Hyva_Checkout')) {
+ $mollieApiClient->addVersionString('HyvaCheckout');
+ }
+
$this->instances[$apiKey] = $mollieApiClient;
return $mollieApiClient;
diff --git a/Service/Mollie/Order/SuccessPageRedirect.php b/Service/Mollie/Order/SuccessPageRedirect.php
new file mode 100644
index 00000000000..a951a35cd84
--- /dev/null
+++ b/Service/Mollie/Order/SuccessPageRedirect.php
@@ -0,0 +1,143 @@
+request = $request;
+ $this->response = $response;
+ $this->checkoutSession = $checkoutSession;
+ $this->eventManager = $eventManager;
+ $this->redirect = $redirect;
+ $this->transactionToOrderRepository = $transactionToOrderRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->config = $config;
+ }
+
+ public function execute(OrderInterface $order, array $orderIds): void
+ {
+ $this->searchCriteriaBuilder->addFilter('transaction_id', $order->getMollieTransactionId());
+ $this->searchCriteriaBuilder->addFilter('order_id', $order->getEntityId());
+ $result = $this->transactionToOrderRepository->getList($this->searchCriteriaBuilder->create());
+
+ // Fallback in case the transaction is not found.
+ if ($result->getTotalCount() === 0) {
+ $this->config->addToLog('warning', [
+ 'message' => 'Transaction not found in the transaction to order table. Redirecting to success page.',
+ 'order_id' => $order->getEntityId(),
+ ]);
+ $this->redirectToSuccessPage($order, $orderIds);
+ return;
+ }
+
+ $items = $result->getItems();
+ /** @var TransactionToOrderInterface $item */
+ $item = array_shift($items);
+
+ if ($item->getRedirected() == 1) {
+ // The user has already been redirected to the success page.
+ $this->redirect->redirect($this->response, 'checkout/cart');
+ return;
+ }
+
+ $item->setRedirected(1);
+ $this->transactionToOrderRepository->save($item);
+
+ $this->redirectToSuccessPage($order, $orderIds);
+ }
+
+ /**
+ * @param OrderInterface $order
+ * @param array $orderIds
+ * @return void
+ */
+ private function redirectToSuccessPage(OrderInterface $order, array $orderIds): void
+ {
+ $this->checkoutSession->setLastOrderId($order->getId());
+ $this->checkoutSession->setLastRealOrderId($order->getIncrementId());
+ $this->checkoutSession->setLastSuccessQuoteId($order->getQuoteId());
+ $this->checkoutSession->setLastQuoteId($order->getQuoteId());
+
+ $redirect = new DataObject([
+ 'path' => 'checkout/onepage/success',
+ 'query' => ['utm_nooverride' => 1],
+ ]);
+
+ $this->eventManager->dispatch('mollie_checkout_success_redirect', [
+ 'redirect' => $redirect,
+ 'order_ids' => $orderIds,
+ 'request' => $this->request,
+ 'response' => $this->response,
+ ]);
+
+ $this->redirect->redirect(
+ $this->response,
+ $redirect->getData('path'),
+ [
+ '_query' => $redirect->getData('query'),
+ '_use_rewrite' => false,
+ ]
+ );
+ }
+}
diff --git a/Service/Mollie/PointOfSaleAvailability.php b/Service/Mollie/PointOfSaleAvailability.php
index 09a9b68f245..07b6b4c6bc4 100644
--- a/Service/Mollie/PointOfSaleAvailability.php
+++ b/Service/Mollie/PointOfSaleAvailability.php
@@ -42,4 +42,14 @@ public function isAvailable(CartInterface $cart): bool
$allowedGroups
);
}
+
+ public function isAvailableForCustomerGroupId(int $customerGroupId, int $storeId): bool
+ {
+ $allowedGroups = explode(',', $this->config->pointofsaleAllowedCustomerGroups($storeId));
+
+ return in_array(
+ (string)$customerGroupId,
+ $allowedGroups
+ );
+ }
}
diff --git a/Service/Mollie/ProcessTransaction.php b/Service/Mollie/ProcessTransaction.php
new file mode 100644
index 00000000000..1edca5fae29
--- /dev/null
+++ b/Service/Mollie/ProcessTransaction.php
@@ -0,0 +1,94 @@
+mollieModel = $mollieModel;
+ $this->orderRepository = $orderRepository;
+ $this->config = $config;
+ $this->transactionToProcessFactory = $transactionToProcessFactory;
+ $this->publishTransactionToProcess = $publishTransactionToProcess;
+ $this->getMollieStatusResultFactory = $getMollieStatusResultFactory;
+ $this->getMollieStatus = $getMollieStatus;
+ }
+
+ public function execute(int $orderId, string $transactionId): GetMollieStatusResult
+ {
+ if ($this->config->processTransactionsInTheQueue()) {
+ $this->queueOrder($orderId, $transactionId);
+ return $this->getMollieStatus->execute($orderId);
+ }
+
+ $order = $this->orderRepository->get($orderId);
+
+ $order->setMollieTransactionId($transactionId);
+ $result = $this->mollieModel->processTransactionForOrder($order, 'webhook');
+
+ return $this->getMollieStatusResultFactory->create([
+ 'status' => $result['status'],
+ 'method' => $order->getPayment()->getMethod(),
+ ]);
+ }
+
+ private function queueOrder(int $orderId, string $transactionId)
+ {
+ /** @var TransactionToProcessInterface $data */
+ $data = $this->transactionToProcessFactory->create();
+ $data->setOrderId($orderId);
+ $data->setTransactionId($transactionId);
+
+ $this->publishTransactionToProcess->publish($data);
+ }
+}
diff --git a/Service/Mollie/ShouldRedirectToSuccessPage.php b/Service/Mollie/ShouldRedirectToSuccessPage.php
deleted file mode 100644
index 1e86e4b400b..00000000000
--- a/Service/Mollie/ShouldRedirectToSuccessPage.php
+++ /dev/null
@@ -1,15 +0,0 @@
- OrderLineType::TYPE_DISCOUNT,
- 'name' => __('Magento Discount'),
+ 'name' => __('Shipping Discount'),
'quantity' => 1,
'unitPrice' => $this->mollieHelper->getAmountArray($currency, -$amount),
'totalAmount' => $this->mollieHelper->getAmountArray($currency, -$amount),
diff --git a/Test/End-2-end/cypress/e2e/magento/checkout.cy.js b/Test/End-2-end/cypress/e2e/magento/checkout.cy.js
index ce40ad2d633..45ccc05e612 100644
--- a/Test/End-2-end/cypress/e2e/magento/checkout.cy.js
+++ b/Test/End-2-end/cypress/e2e/magento/checkout.cy.js
@@ -93,4 +93,25 @@ describe('Checkout usage', () => {
cy.get('.mollie-checkout-type').should('contain', 'Order');
});
+
+ it('C2530311: Validate that the success page can only be visited once', () => {
+ visitCheckoutPayment.visit();
+
+ checkoutPaymentPage.selectPaymentMethod('iDeal');
+ checkoutPaymentPage.selectFirstAvailableIssuer();
+
+ cy.intercept('mollie/checkout/process/*').as('processAction');
+
+ checkoutPaymentPage.placeOrder();
+
+ mollieHostedPaymentPage.selectStatus('paid');
+
+ checkoutSuccessPage.assertThatOrderSuccessPageIsShown();
+
+ cy.wait('@processAction').then((interception) => {
+ cy.visit(interception.request.url);
+ });
+
+ cy.url().should('include', 'checkout/cart');
+ });
})
diff --git a/Test/Fakes/Queue/Publisher/PublishTransactionToProcessFake.php b/Test/Fakes/Queue/Publisher/PublishTransactionToProcessFake.php
new file mode 100644
index 00000000000..fb6e4068dc8
--- /dev/null
+++ b/Test/Fakes/Queue/Publisher/PublishTransactionToProcessFake.php
@@ -0,0 +1,40 @@
+timesCalled;
+ }
+
+ public function preventPublish(): void
+ {
+ $this->publish = false;
+ }
+
+ public function publish(TransactionToProcessInterface $data): void
+ {
+ $this->timesCalled++;
+
+ if (!$this->publish) {
+ return;
+ }
+
+ parent::publish($data);
+ }
+}
diff --git a/Test/Fakes/Service/Mollie/GetMollieStatusFake.php b/Test/Fakes/Service/Mollie/GetMollieStatusFake.php
new file mode 100644
index 00000000000..f891aeba2f8
--- /dev/null
+++ b/Test/Fakes/Service/Mollie/GetMollieStatusFake.php
@@ -0,0 +1,34 @@
+response = $response;
+ }
+
+ public function execute(int $orderId): GetMollieStatusResult
+ {
+ if ($this->response) {
+ return $this->response;
+ }
+
+ return parent::execute($orderId);
+ }
+}
diff --git a/Test/Fakes/Service/Mollie/ProcessTransactionFake.php b/Test/Fakes/Service/Mollie/ProcessTransactionFake.php
new file mode 100644
index 00000000000..d45f73009d0
--- /dev/null
+++ b/Test/Fakes/Service/Mollie/ProcessTransactionFake.php
@@ -0,0 +1,43 @@
+timesCalled;
+ }
+
+ public function setResponse(GetMollieStatusResult $response): void
+ {
+ $this->response = $response;
+ }
+
+ public function execute(int $orderId, string $transactionId): GetMollieStatusResult
+ {
+ $this->timesCalled++;
+
+ if ($this->response) {
+ return $this->response;
+ }
+
+ return parent::execute($orderId, $transactionId); // TODO: Change the autogenerated stub
+ }
+}
diff --git a/Test/Integration/Controller/Checkout/ProcessTest.php b/Test/Integration/Controller/Checkout/ProcessTest.php
index 5b8af9c6254..78c54eb90ce 100644
--- a/Test/Integration/Controller/Checkout/ProcessTest.php
+++ b/Test/Integration/Controller/Checkout/ProcessTest.php
@@ -12,8 +12,11 @@
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\TestFramework\TestCase\AbstractController;
use Mollie\Payment\Model\Mollie;
+use Mollie\Payment\Service\Mollie\GetMollieStatusResult;
+use Mollie\Payment\Service\Mollie\ProcessTransaction;
use Mollie\Payment\Service\Mollie\ValidateProcessRequest;
use Mollie\Payment\Test\Fakes\Service\Mollie\FakeValidateProcessRequest;
+use Mollie\Payment\Test\Fakes\Service\Mollie\ProcessTransactionFake;
class ProcessTest extends AbstractController
{
@@ -51,12 +54,17 @@ public function testUsesOrderIdParameter()
$order = $this->loadOrderById('100000001');
$this->fakeValidation([(string)$order->getId() => 'abc']);
- $mollieModel = $this->createMock(Mollie::class);
- $mollieModel->expects($this->once())->method('processTransaction')->with($order->getId())->willReturn([]);
+ $fake = $this->_objectManager->create(ProcessTransactionFake::class);
+ $fake->setResponse($this->_objectManager->create(
+ GetMollieStatusResult::class,
+ ['status' => 'paid', 'method' => 'ideal']
+ ));
- $this->_objectManager->addSharedInstance($mollieModel, Mollie::class);
+ $this->_objectManager->addSharedInstance($fake, ProcessTransaction::class);
$this->dispatch('mollie/checkout/process?order_id=' . $order->getId());
+
+ $this->assertEquals(1, $fake->getTimesCalled());
}
/**
@@ -76,10 +84,13 @@ public function testUsesOrderIdsParameter()
(string)$order4->getId() => 'jkl',
]);
- $mollieModel = $this->createMock(Mollie::class);
- $mollieModel->expects($this->exactly(4))->method('processTransaction')->willReturn([]);
+ $fake = $this->_objectManager->create(ProcessTransactionFake::class);
+ $fake->setResponse($this->_objectManager->create(
+ GetMollieStatusResult::class,
+ ['status' => 'paid', 'method' => 'ideal']
+ ));
- $this->_objectManager->addSharedInstance($mollieModel, Mollie::class);
+ $this->_objectManager->addSharedInstance($fake, ProcessTransaction::class);
$queryString = [
'order_ids[]=' . $order1->getId(),
@@ -89,6 +100,8 @@ public function testUsesOrderIdsParameter()
];
$this->dispatch('mollie/checkout/process?' . implode('&', $queryString));
+
+ $this->assertEquals(4, $fake->getTimesCalled());
}
/**
@@ -103,7 +116,11 @@ private function loadOrderById($orderId)
$orderList = $repository->getList($searchCriteria)->getItems();
- return array_shift($orderList);
+ $order = array_shift($orderList);
+ $order->setMollieTransactionId('ord_abc' . $orderId);
+ $repository->save($order);
+
+ return $order;
}
private function fakeValidation(array $response): void
diff --git a/Test/Integration/GraphQL/Resolver/Checkout/ProcessTransactionTest.php b/Test/Integration/GraphQL/Resolver/Checkout/ProcessTransactionTest.php
index 31e6da5d7c5..5caf97315ed 100644
--- a/Test/Integration/GraphQL/Resolver/Checkout/ProcessTransactionTest.php
+++ b/Test/Integration/GraphQL/Resolver/Checkout/ProcessTransactionTest.php
@@ -10,7 +10,10 @@
use Magento\Quote\Api\Data\CartInterface;
use Magento\Quote\Model\Quote;
use Mollie\Payment\Model\Mollie;
+use Mollie\Payment\Service\Mollie\GetMollieStatusResult;
+use Mollie\Payment\Service\Mollie\ProcessTransaction;
use Mollie\Payment\Service\PaymentToken\Generate;
+use Mollie\Payment\Test\Fakes\Service\Mollie\ProcessTransactionFake;
use Mollie\Payment\Test\Integration\GraphQLTestCase;
/**
@@ -32,11 +35,17 @@ public function testResetsTheCartWhenPending()
$order = $this->loadOrder('100000001');
$order->setQuoteId($cart->getId());
+ $order->setMollieTransactionId('tr_123');
+ $order->save();
$tokenModel = $this->objectManager->get(Generate::class)->forOrder($order);
- $mollieMock = $this->createMock(Mollie::class);
- $mollieMock->method('processTransaction')->willReturn(['status' => 'failed']);
- $this->objectManager->addSharedInstance($mollieMock, Mollie::class);
+
+ $fake = $this->objectManager->create(ProcessTransactionFake::class);
+ $fake->setResponse($this->objectManager->create(
+ GetMollieStatusResult::class,
+ ['status' => 'failed', 'method' => 'ideal']
+ ));
+ $this->objectManager->addSharedInstance($fake, ProcessTransaction::class);
$result = $this->graphQlQuery('mutation {
mollieProcessTransaction(input: { payment_token: "' . $tokenModel->getToken() . '" }) {
@@ -65,11 +74,17 @@ public function testDoesNotReactivateTheCartWhenTheStatusIsPending()
$order = $this->loadOrder('100000001');
$order->setQuoteId($cart->getId());
+ $order->setMollieTransactionId('tr_123');
+ $order->save();
$tokenModel = $this->objectManager->get(Generate::class)->forOrder($order);
- $mollieMock = $this->createMock(Mollie::class);
- $mollieMock->method('processTransaction')->willReturn(['status' => 'pending', 'success' => true]);
- $this->objectManager->addSharedInstance($mollieMock, Mollie::class);
+
+ $fake = $this->objectManager->create(ProcessTransactionFake::class);
+ $fake->setResponse($this->objectManager->create(
+ GetMollieStatusResult::class,
+ ['status' => 'pending', 'method' => 'ideal']
+ ));
+ $this->objectManager->addSharedInstance($fake, ProcessTransaction::class);
$result = $this->graphQlQuery('mutation {
mollieProcessTransaction(input: { payment_token: "' . $tokenModel->getToken() . '" }) {
diff --git a/Test/Integration/Model/OrderLinesTest.php b/Test/Integration/Model/OrderLinesTest.php
index 1cf85952ebb..7410cd21de0 100644
--- a/Test/Integration/Model/OrderLinesTest.php
+++ b/Test/Integration/Model/OrderLinesTest.php
@@ -57,8 +57,10 @@ public function testCreditmemoUsesTheDiscount()
/** @var CreditmemoItemInterface $creditmemoItem */
$creditmemoItem = $this->objectManager->create(CreditmemoItemInterface::class);
- $creditmemoItem->setBaseRowTotalInclTax(45);
+ $creditmemoItem->setBaseRowTotal(45); // 45 - 21% tax
+ $creditmemoItem->setBaseRowTotalInclTax(45 * 1.21);
$creditmemoItem->setBaseDiscountAmount(9);
+ $creditmemoItem->setBaseTaxAmount(7.56); // 21% tax
$creditmemoItem->setQty(1);
$creditmemoItem->setOrderItemId(999);
@@ -75,7 +77,7 @@ public function testCreditmemoUsesTheDiscount()
$this->assertCount(1, $result['lines']);
$line = $result['lines'][0];
- $this->assertEquals(36, $line['amount']['value']);
+ $this->assertEquals(45 - 9 + 7.56, $line['amount']['value']);
$this->assertEquals(1, $line['quantity']);
}
@@ -143,7 +145,9 @@ public function testGetShipmentOrderLinesAddsAnAmountWhenTheOrderHasAnDiscount()
/** @var OrderItemInterface $orderItem */
$orderItem = $item->getOrderItem();
- $orderItem->setBaseRowTotalInclTax(100);
+ $orderItem->setBaseRowTotal(100);
+ $orderItem->setBaseTaxAmount(21);
+ $orderItem->setBaseRowTotalInclTax(121);
$orderItem->setBaseDiscountAmount(30);
$orderItem->setQtyOrdered(10);
}
@@ -158,12 +162,13 @@ public function testGetShipmentOrderLinesAddsAnAmountWhenTheOrderHasAnDiscount()
$this->assertEquals('EUR', $result['lines'][0]['amount']['currency']);
// 100 euro subtotal
+ // 21 euro tax
// 30 discount
// 70 grand total
// 10 items = 10 euro each
// 2 items ordered
- // ((100 - 30) / 10) * 2 = 14
- $this->assertEquals(14, $result['lines'][0]['amount']['value']);
+ // ((100 + 21 - 30) / 10) * 2 = 14
+ $this->assertEquals(18.2, $result['lines'][0]['amount']['value']);
}
public function tearDownWithoutVoid()
diff --git a/Test/Integration/Service/Mollie/Order/SuccessPageRedirectTest.php b/Test/Integration/Service/Mollie/Order/SuccessPageRedirectTest.php
new file mode 100644
index 00000000000..c3952e8c660
--- /dev/null
+++ b/Test/Integration/Service/Mollie/Order/SuccessPageRedirectTest.php
@@ -0,0 +1,80 @@
+loadOrder('100000001');
+ $order->setMollieTransactionId($transactionId);
+
+ $transactionToOrder = $this->objectManager->create(TransactionToOrderInterface::class);
+ $transactionToOrder->setOrderId((int)$order->getEntityId());
+ $transactionToOrder->setTransactionId($transactionId);
+ $transactionToOrder->setRedirected(0); // This is the default but being explicit
+ $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder);
+
+ $instance = $this->objectManager->create(SuccessPageRedirect::class);
+ $instance->execute($order, [$order->getEntityId()]);
+
+ /** @var ResponseInterface $response */
+ $response = $this->objectManager->get(ResponseInterface::class);
+ /** @var \Laminas\Http\Headers $headers */
+ $headers = $response->getHeaders();
+
+ $this->assertStringContainsString(
+ 'checkout/onepage/success',
+ $headers->get('Location')->getFieldValue()
+ );
+ }
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ * @return void
+ */
+ public function testRedirectsToCartWhenAlreadyRedirected(): void
+ {
+ $transactionId = 'tr_abc123';
+ $order = $this->loadOrder('100000001');
+ $order->setMollieTransactionId($transactionId);
+
+ $transactionToOrder = $this->objectManager->create(TransactionToOrderInterface::class);
+ $transactionToOrder->setOrderId((int)$order->getEntityId());
+ $transactionToOrder->setTransactionId($transactionId);
+
+ // Mark it as already redirected
+ $transactionToOrder->setRedirected(1);
+
+ $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder);
+
+ $instance = $this->objectManager->create(SuccessPageRedirect::class);
+ $instance->execute($order, [$order->getEntityId()]);
+
+ /** @var ResponseInterface $response */
+ $response = $this->objectManager->get(ResponseInterface::class);
+ /** @var \Laminas\Http\Headers $headers */
+ $headers = $response->getHeaders();
+
+ $this->assertStringContainsString(
+ 'checkout/cart',
+ $headers->get('Location')->getFieldValue()
+ );
+ }
+}
diff --git a/Test/Integration/Service/Mollie/ProcessTransactionTest.php b/Test/Integration/Service/Mollie/ProcessTransactionTest.php
new file mode 100644
index 00000000000..45b88fcfd9f
--- /dev/null
+++ b/Test/Integration/Service/Mollie/ProcessTransactionTest.php
@@ -0,0 +1,66 @@
+loadOrderById('100000001');
+
+ $fake = $this->objectManager->create(PublishTransactionToProcessFake::class);
+ $this->objectManager->addSharedInstance($fake, PublishTransactionToProcess::class);
+
+ $mollieMock = $this->createMock(Mollie::class);
+ $mollieMock->method('processTransactionForOrder')->willReturn(['status' => 'paid', 'method' => 'ideal']);
+
+ $this->objectManager->addSharedInstance($mollieMock, Mollie::class);
+
+ $instance = $this->objectManager->create(ProcessTransaction::class);
+ $instance->execute((int)$order->getId(), 'tr_123');
+
+ $this->assertEquals(0, $fake->getTimesCalled());
+ }
+
+ /**
+ * @magentoConfigFixture current_store payment/mollie_general/process_transactions_in_the_queue 1
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ */
+ public function testPublishesTask(): void
+ {
+ $order = $this->loadOrderById('100000001');
+
+ $publisherFake = $this->objectManager->create(PublishTransactionToProcessFake::class);
+ $this->objectManager->addSharedInstance($publisherFake, PublishTransactionToProcess::class);
+
+ $mollieStatusFake = $this->objectManager->create(GetMollieStatusFake::class);
+ $mollieStatusFake->setResponse($this->objectManager->create(GetMollieStatusResult::class, [
+ 'status' => 'paid',
+ 'method' => 'ideal',
+ ]));
+ $this->objectManager->addSharedInstance($mollieStatusFake, GetMollieStatus::class);
+
+ $instance = $this->objectManager->create(ProcessTransaction::class);
+ $instance->execute((int)$order->getId(), 'tr_123');
+
+ $this->assertEquals(1, $publisherFake->getTimesCalled());
+ }
+}
diff --git a/Test/Integration/Service/Mollie/ShouldRedirectToSuccessPageTest.php b/Test/Integration/Service/Mollie/ShouldRedirectToSuccessPageTest.php
deleted file mode 100644
index 9c8a450acca..00000000000
--- a/Test/Integration/Service/Mollie/ShouldRedirectToSuccessPageTest.php
+++ /dev/null
@@ -1,42 +0,0 @@
-objectManager->get(ShouldRedirectToSuccessPage::class);
-
- $result = $instance->execute([
- // Omitting the success key on purpose
- ]);
-
- $this->assertFalse($result);
- }
-
- public function testNotRedirectWhenTheSuccessKeyExistsButIsFalse(): void
- {
- $instance = $this->objectManager->get(ShouldRedirectToSuccessPage::class);
-
- $result = $instance->execute([
- 'success' => false,
- ]);
-
- $this->assertFalse($result);
- }
-
- public function testRedirectWhenTheSuccessKeyExistsAndIsTrue(): void
- {
- $instance = $this->objectManager->get(ShouldRedirectToSuccessPage::class);
-
- $result = $instance->execute([
- 'success' => true,
- ]);
-
- $this->assertTrue($result);
- }
-}
diff --git a/Test/Integration/Webapi/GetPaymentLinkRedirectTest.php b/Test/Integration/Webapi/GetPaymentLinkRedirectTest.php
new file mode 100644
index 00000000000..6b6e0fae0f5
--- /dev/null
+++ b/Test/Integration/Webapi/GetPaymentLinkRedirectTest.php
@@ -0,0 +1,73 @@
+loadOrder('100000001');
+ $order->setState(Order::STATE_PENDING_PAYMENT);
+ $order->save();
+
+ $mollieMock = $this->createMock(Mollie::class);
+ $mollieMock->method('startTransaction')->willReturn('https://www.mollie.com');
+ $this->objectManager->addSharedInstance($mollieMock, Mollie::class);
+
+ $encryptor = $this->objectManager->get(EncryptorInterface::class);
+
+ $hash = base64_encode($encryptor->encrypt((string)$order->getEntityId()));
+
+ $instance = $this->objectManager->create(GetPaymentLinkRedirect::class);
+ $result = $instance->byHash($hash);
+
+ $this->assertFalse($result->isAlreadyPaid());
+ $this->assertEquals('https://www.mollie.com', $result->getRedirectUrl());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ * @return void
+ */
+ public function testDoesNotIncludeLinkWhenAlreadyPaid(): void
+ {
+ $order = $this->loadOrder('100000001');
+ $order->setState(Order::STATE_PROCESSING);
+ $order->save();
+
+ $encryptor = $this->objectManager->get(EncryptorInterface::class);
+ $hash = base64_encode($encryptor->encrypt((string)$order->getEntityId()));
+
+ $instance = $this->objectManager->create(GetPaymentLinkRedirect::class);
+ $result = $instance->byHash($hash);
+
+ $this->assertTrue($result->isAlreadyPaid());
+ $this->assertEquals('', $result->getRedirectUrl());
+ }
+}
diff --git a/Webapi/GetCustomerOrder.php b/Webapi/GetCustomerOrder.php
index 58f78d8be20..08a971f9ac2 100644
--- a/Webapi/GetCustomerOrder.php
+++ b/Webapi/GetCustomerOrder.php
@@ -9,6 +9,8 @@
use Magento\Framework\Encryption\Encryptor;
use Magento\Sales\Api\OrderRepositoryInterface;
use Mollie\Payment\Api\Webapi\GetCustomerOrderInterface;
+use Mollie\Payment\Service\Mollie\GetMollieStatus;
+use Mollie\Payment\Service\Mollie\GetMollieStatusResult;
class GetCustomerOrder implements GetCustomerOrderInterface
{
@@ -16,18 +18,23 @@ class GetCustomerOrder implements GetCustomerOrderInterface
* @var Encryptor
*/
private $encryptor;
-
/**
* @var OrderRepositoryInterface
*/
private $orderRepository;
+ /**
+ * @var GetMollieStatus
+ */
+ private $getMollieStatus;
public function __construct(
Encryptor $encryptor,
- OrderRepositoryInterface $orderRepository
+ OrderRepositoryInterface $orderRepository,
+ GetMollieStatus $getMollieStatus
) {
$this->encryptor = $encryptor;
$this->orderRepository = $orderRepository;
+ $this->getMollieStatus = $getMollieStatus;
}
/**
@@ -42,14 +49,33 @@ public function byHash(string $hash): array
$orderId = $this->encryptor->decrypt($decodedHash);
$order = $this->orderRepository->get($orderId);
+ $mollieResult = $this->getMollieStatus->execute($orderId);
+
return [
[
'id' => $order->getEntityId(),
'increment_id' => $order->getIncrementId(),
'created_at' => $order->getCreatedAt(),
'grand_total' => $order->getGrandTotal(),
- 'status' => $order->getStatus(),
+ 'status' => $this->mapMollieStatusToMagentoStatus($mollieResult),
]
];
}
-}
\ No newline at end of file
+
+ public function mapMollieStatusToMagentoStatus(GetMollieStatusResult $mollieResult): string
+ {
+ if (in_array($mollieResult->getStatus(), ['paid', 'authorized'])) {
+ return 'processing';
+ }
+
+ if (in_array($mollieResult->getStatus(), ['canceled', 'expired', 'failed'])) {
+ return 'canceled';
+ }
+
+ if (in_array($mollieResult->getStatus(), ['completed'])) {
+ return 'complete';
+ }
+
+ return 'pending';
+ }
+}
diff --git a/Webapi/GetPaymentLinkRedirect.php b/Webapi/GetPaymentLinkRedirect.php
new file mode 100644
index 00000000000..4427cae02a3
--- /dev/null
+++ b/Webapi/GetPaymentLinkRedirect.php
@@ -0,0 +1,30 @@
+paymentLinkRedirect = $paymentLinkRedirect;
+ }
+
+ public function byHash(string $hash): PaymentLinkRedirectResultInterface
+ {
+ return $this->paymentLinkRedirect->execute($hash);
+ }
+}
diff --git a/Webapi/PaymentInformationMeta.php b/Webapi/PaymentInformationMeta.php
index 5f1e09faf27..ee3c70ee6f9 100644
--- a/Webapi/PaymentInformationMeta.php
+++ b/Webapi/PaymentInformationMeta.php
@@ -17,6 +17,7 @@
use Mollie\Payment\Api\Webapi\PaymentInformationMetaInterface;
use Mollie\Payment\Block\Form\Pointofsale;
use Mollie\Payment\Config;
+use Mollie\Payment\Service\Mollie\AvailableTerminals;
use Mollie\Payment\Service\Mollie\GetIssuers;
use Mollie\Payment\Service\Mollie\MollieApiClient;
use Mollie\Payment\Service\Mollie\PaymentMethods;
@@ -43,6 +44,10 @@ class PaymentInformationMeta implements PaymentInformationMetaInterface
* @var Pointofsale
*/
private $pointofsale;
+ /**
+ * @var AvailableTerminals
+ */
+ private $availableTerminals;
/**
* @var Config
*/
@@ -63,6 +68,7 @@ public function __construct(
PaymentMethods $paymentMethods,
GetIssuers $getIssuers,
Pointofsale $pointofsale,
+ AvailableTerminals $availableTerminals,
IssuerInterfaceFactory $issuerFactory,
TerminalInterfaceFactory $terminalFactory
) {
@@ -71,6 +77,7 @@ public function __construct(
$this->paymentMethods = $paymentMethods;
$this->getIssuers = $getIssuers;
$this->pointofsale = $pointofsale;
+ $this->availableTerminals = $availableTerminals;
$this->config = $config;
$this->issuerFactory = $issuerFactory;
$this->terminalFactory = $terminalFactory;
@@ -119,6 +126,6 @@ private function getTerminals(string $code): array
return array_map(function (array $terminal) {
return $this->terminalFactory->create($terminal);
- }, $this->pointofsale->getTerminals());
+ }, $this->availableTerminals->execute());
}
}
diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml
index 56e02c17273..37c2762f8fa 100644
--- a/etc/adminhtml/system.xml
+++ b/etc/adminhtml/system.xml
@@ -51,7 +51,7 @@
- Mollie\Payment\Model\Adminhtml\Backend\FlushMollieCache
+ Mollie\Payment\Model\Adminhtml\Backend\ChangeApiMode
Mollie\Payment\Model\Adminhtml\Source\ApiKey
payment/mollie_general/type
@@ -69,9 +69,12 @@
payment/mollie_general/apikey_live
-
+ Mollie\Payment\Model\Adminhtml\Backend\DoNoUpdate
+ Mollie\Payment\Block\Adminhtml\System\Config\Form\DisabledInput
+ When you save the api key or change the mode, this value is automatically updated.
payment/mollie_general/profileid
custom_url
-
+
+ payment/mollie_general/process_transactions_in_the_queue
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
Magento\Config\Model\Config\Source\Yesno
diff --git a/etc/communication.xml b/etc/communication.xml
new file mode 100644
index 00000000000..68d76b733fd
--- /dev/null
+++ b/etc/communication.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/etc/db_schema.xml b/etc/db_schema.xml
index f2c763834b2..e3cdd0120c4 100644
--- a/etc/db_schema.xml
+++ b/etc/db_schema.xml
@@ -117,6 +117,7 @@
+
diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json
index 57851c49868..32454b15b22 100644
--- a/etc/db_schema_whitelist.json
+++ b/etc/db_schema_whitelist.json
@@ -84,6 +84,7 @@
"transaction_id": true,
"order_id": true,
"skipped": true,
+ "redirected": true,
"created_at": true
},
"constaint": {
diff --git a/etc/di.xml b/etc/di.xml
index 6eca105e668..312a24c85ca 100644
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -3,6 +3,7 @@
+
@@ -32,6 +33,10 @@
+
+
+
+
Magento\Framework\App\ProductMetadataInterface\Proxy
diff --git a/etc/module.xml b/etc/module.xml
index 55689b794e5..0a8d8095346 100644
--- a/etc/module.xml
+++ b/etc/module.xml
@@ -16,6 +16,7 @@
+
diff --git a/etc/queue_consumer.xml b/etc/queue_consumer.xml
new file mode 100644
index 00000000000..0d28f8917e8
--- /dev/null
+++ b/etc/queue_consumer.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/etc/queue_publisher.xml b/etc/queue_publisher.xml
new file mode 100644
index 00000000000..f6545991324
--- /dev/null
+++ b/etc/queue_publisher.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/etc/queue_topology.xml b/etc/queue_topology.xml
new file mode 100644
index 00000000000..ec51af31a8b
--- /dev/null
+++ b/etc/queue_topology.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/etc/schema.graphqls b/etc/schema.graphqls
index 411f19e7f83..32fdf3903a8 100644
--- a/etc/schema.graphqls
+++ b/etc/schema.graphqls
@@ -24,6 +24,7 @@ type Cart {
type AvailablePaymentMethod {
mollie_available_issuers: [MollieIssuer!] @resolver(class: "Mollie\\Payment\\GraphQL\\Resolver\\Cart\\AvailableIssuersForMethod") @doc(description: "Available issuers for this payment method")
+ mollie_available_terminals: [MollieTerminalOutput!] @resolver(class: "Mollie\\Payment\\GraphQL\\Resolver\\Cart\\AvailableTerminalsForMethod") @doc(description: "Available terminals for this payment method")
mollie_meta: MolliePaymentMethodMeta! @resolver(class: "Mollie\\Payment\\GraphQL\\Resolver\\Cart\\PaymentMethodMeta") @doc(description: "Retrieve meta information for this payment method (image)")
}
@@ -73,6 +74,14 @@ type Query {
molliePaymentMethods(input: MolliePaymentMethodsInput): MolliePaymentMethodsOutput @resolver(class: "Mollie\\Payment\\GraphQL\\Resolver\\General\\MolliePaymentMethods") @cache(cacheIdentity: "Mollie\\Payment\\GraphQL\\Resolver\\Cache\\PaymentMethodsCache")
}
+type MollieTerminalOutput {
+ id: String!
+ brand: String!
+ model: String!
+ serialNumber: String
+ description: String!
+}
+
type MollieResetCartOutput {
cart: Cart!
}
diff --git a/etc/webapi.xml b/etc/webapi.xml
index 0c8bcee049b..7461462ccac 100644
--- a/etc/webapi.xml
+++ b/etc/webapi.xml
@@ -51,6 +51,13 @@
+
+
+
+
+
+
+
diff --git a/view/adminhtml/layout/adminhtml_order_shipment_new.xml b/view/adminhtml/layout/adminhtml_order_shipment_new.xml
index 8121473ace0..c5a3b59d575 100644
--- a/view/adminhtml/layout/adminhtml_order_shipment_new.xml
+++ b/view/adminhtml/layout/adminhtml_order_shipment_new.xml
@@ -10,6 +10,7 @@