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 @@ diff --git a/view/adminhtml/layout/sales_order_create_index.xml b/view/adminhtml/layout/sales_order_create_index.xml new file mode 100644 index 00000000000..3825f38ea23 --- /dev/null +++ b/view/adminhtml/layout/sales_order_create_index.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/view/adminhtml/templates/form/mollie_paymentlink_javascript.phtml b/view/adminhtml/templates/form/mollie_paymentlink_javascript.phtml new file mode 100644 index 00000000000..f99f16fa7ed --- /dev/null +++ b/view/adminhtml/templates/form/mollie_paymentlink_javascript.phtml @@ -0,0 +1,49 @@ + +