diff --git a/.github/workflows/end-2-end-test.yml b/.github/workflows/end-2-end-test.yml index adf7832aa9a..43455ac0f36 100644 --- a/.github/workflows/end-2-end-test.yml +++ b/.github/workflows/end-2-end-test.yml @@ -62,7 +62,7 @@ jobs: CYPRESS_TESTRAIL_PASSWORD: ${{ secrets.TESTRAIL_PASSWORD }} CYPRESS_TESTRAIL_PROJECT_ID: 5 CYPRESS_TESTRAIL_MILESTONE_ID: 37 - CYPRESS_TESTRAIL_RUN_NAME: "Github Workflow __datetime__, ${{ github.event.head_commit.message }}, PHP version: ${{ matrix.PHP_VERSION }}, Magento version: ${{ matrix.MAGENTO_VERSION }}" + CYPRESS_TESTRAIL_RUN_NAME: "Github Workflow, ${{ github.head_ref || github.ref_name }}, PHP version: ${{ matrix.PHP_VERSION }}, Magento version: ${{ matrix.MAGENTO_VERSION }}" CYPRESS_TESTRAIL_RUN_CLOSE: true steps: - uses: actions/checkout@v2 diff --git a/Config.php b/Config.php index 6e94b71a81a..52f5465176a 100644 --- a/Config.php +++ b/Config.php @@ -8,7 +8,6 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ProductMetadataInterface; -use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Module\Manager; use Magento\Store\Model\ScopeInterface; use Mollie\Payment\Logger\MollieLogger; @@ -17,6 +16,8 @@ class Config { const EXTENSION_CODE = 'Mollie_Payment'; + const ADVANCED_INVOICE_MOMENT = 'payment/mollie_general/invoice_moment'; + const ADVANCED_ENABLE_MANUAL_CAPTURE = 'payment/mollie_general/enable_manual_capture'; const GENERAL_ENABLED = 'payment/mollie_general/enabled'; const GENERAL_APIKEY_LIVE = 'payment/mollie_general/apikey_live'; const GENERAL_APIKEY_TEST = 'payment/mollie_general/apikey_test'; @@ -33,6 +34,7 @@ class Config 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'; + const GENERAL_INVOICE_NOTIFY_KLARNA = 'payment/mollie_general/invoice_notify_klarna'; const GENERAL_LOCALE = 'payment/mollie_general/locale'; const GENERAL_ORDER_STATUS_PENDING = 'payment/mollie_general/order_status_pending'; const GENERAL_PROFILEID = 'payment/mollie_general/profileid'; @@ -85,11 +87,6 @@ class Config */ private $moduleManager; - /** - * @var EncryptorInterface - */ - private $encryptor; - /** * @var ProductMetadataInterface */ @@ -99,13 +96,11 @@ public function __construct( ScopeConfigInterface $config, MollieLogger $logger, Manager $moduleManager, - EncryptorInterface $encryptor, ProductMetadataInterface $productMetadata ) { $this->config = $config; $this->logger = $logger; $this->moduleManager = $moduleManager; - $this->encryptor = $encryptor; $this->productMetadata = $productMetadata; } @@ -126,7 +121,7 @@ private function getPath($path, $storeId, $scope = ScopeInterface::SCOPE_STORE) * @param string $scope * @return bool */ - private function isSetFlag($path, $storeId, $scope = ScopeInterface::SCOPE_STORE) + private function isSetFlag($path, $storeId, string $scope = ScopeInterface::SCOPE_STORE): bool { return $this->config->isSetFlag($path, $scope, $storeId); } @@ -242,6 +237,24 @@ public function isDebugMode($storeId = null) return $this->isSetFlag(static::GENERAL_DEBUG, $storeId); } + /** + * @param null|int|string $storeId + * @return string|null + */ + public function getInvoiceMoment($storeId = null): ?string + { + return $this->getPath(static::ADVANCED_INVOICE_MOMENT, $storeId); + } + + /** + * @param null|int|string $storeId + * @return bool + */ + public function useManualCapture($storeId): bool + { + return $this->isSetFlag(static::ADVANCED_ENABLE_MANUAL_CAPTURE, $storeId); + } + /** * @param null|int|string $storeId * @return bool @@ -251,6 +264,15 @@ public function sendInvoiceEmail($storeId = null) return $this->isSetFlag(static::GENERAL_INVOICE_NOTIFY, $storeId); } + /** + * @param null|int|string $storeId + * @return bool + */ + public function sendInvoiceEmailForKlarna($storeId = null) + { + return $this->isSetFlag(static::GENERAL_INVOICE_NOTIFY_KLARNA, $storeId); + } + /** * @param null|int|string $storeId * @return string diff --git a/Model/Client/Orders.php b/Model/Client/Orders.php index c97677ffce0..dad1cbdff0f 100644 --- a/Model/Client/Orders.php +++ b/Model/Client/Orders.php @@ -34,6 +34,7 @@ use Mollie\Payment\Service\Mollie\Order\RefundUsingPayment; use Mollie\Payment\Service\Mollie\Order\Transaction\Expires; use Mollie\Payment\Service\Order\BuildTransaction; +use Mollie\Payment\Service\Order\Invoice\ShouldEmailInvoice; use Mollie\Payment\Service\Order\Lines\StoreCredit; use Mollie\Payment\Service\Order\OrderCommentHistory; use Mollie\Payment\Service\Order\PartialInvoice; @@ -151,6 +152,10 @@ class Orders extends AbstractModel * @var OrderLockService */ private $orderLockService; + /** + * @var ShouldEmailInvoice + */ + private $shouldEmailInvoice; /** * Orders constructor. @@ -178,6 +183,7 @@ class Orders extends AbstractModel * @param EventManager $eventManager * @param LinkTransactionToOrder $linkTransactionToOrder * @param OrderLockService $orderLockService + * @param ShouldEmailInvoice $shouldEmailInvoice */ public function __construct( OrderLines $orderLines, @@ -202,7 +208,8 @@ public function __construct( Config $config, EventManager $eventManager, LinkTransactionToOrder $linkTransactionToOrder, - OrderLockService $orderLockService + OrderLockService $orderLockService, + ShouldEmailInvoice $shouldEmailInvoice ) { $this->orderLines = $orderLines; $this->invoiceSender = $invoiceSender; @@ -227,6 +234,7 @@ public function __construct( $this->config = $config; $this->linkTransactionToOrder = $linkTransactionToOrder; $this->orderLockService = $orderLockService; + $this->shouldEmailInvoice = $shouldEmailInvoice; } /** @@ -557,7 +565,7 @@ public function createShipment(Order\Shipment $shipment, OrderInterface $order) $this->orderRepository->save($order); - $sendInvoice = $this->mollieHelper->sendInvoice($order->getStoreId()); + $sendInvoice = $this->shouldEmailInvoice->execute((int)$order->getStoreId(), $payment->getMethod()); if ($invoice && $invoice->getId() && !$invoice->getEmailSent() && $sendInvoice) { $this->invoiceSender->send($invoice); $message = __('Notified customer about invoice #%1', $invoice->getIncrementId()); @@ -689,9 +697,12 @@ public function createOrderRefund(Order\Creditmemo $creditmemo, Order $order) /** @var int|float $remainderAmount */ $remainderAmount = $order->getPayment()->getAdditionalInformation('remainder_amount'); + $grandTotal = $this->config->useBaseCurrency($storeId) ? + $creditmemo->getBaseGrandTotal() : + $creditmemo->getGrandTotal(); $maximumAmountToRefund = $order->getBaseGrandTotal() - $remainderAmount; if ($remainderAmount) { - $amount = $creditmemo->getBaseGrandTotal() > $maximumAmountToRefund ? $maximumAmountToRefund : $creditmemo->getBaseGrandTotal(); + $amount = $grandTotal > $maximumAmountToRefund ? $maximumAmountToRefund : $grandTotal; $this->refundUsingPayment->execute( $mollieApi, @@ -710,7 +721,7 @@ public function createOrderRefund(Order\Creditmemo $creditmemo, Order $order) $mollieApi, $transactionId, $creditmemo->getOrderCurrencyCode(), - $creditmemo->getBaseGrandTotal() + $grandTotal ); return $this; diff --git a/Model/Client/Orders/ProcessTransaction.php b/Model/Client/Orders/ProcessTransaction.php index eb616faf121..21ae7c43a3a 100644 --- a/Model/Client/Orders/ProcessTransaction.php +++ b/Model/Client/Orders/ProcessTransaction.php @@ -163,6 +163,10 @@ public function execute( return $this->orderProcessors->process('expired', $order, $mollieOrder, $type, $defaultResponse); } + if ($mollieOrder->isShipping()) { + return $this->orderProcessors->process('shipping', $order, $mollieOrder, $type, $defaultResponse); + } + throw new LocalizedException(__('Unable to process order %s', $order->getIncrementId())); } } diff --git a/Model/Client/Payments.php b/Model/Client/Payments.php index f01b5362a5f..4251d29243a 100644 --- a/Model/Client/Payments.php +++ b/Model/Client/Payments.php @@ -12,13 +12,16 @@ use Magento\Framework\Model\AbstractModel; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\OrderRepository; use Mollie\Api\MollieApiClient; use Mollie\Api\Resources\Payment as MolliePayment; use Mollie\Api\Types\PaymentStatus; use Mollie\Payment\Helper\General as MollieHelper; +use Mollie\Payment\Model\Adminhtml\Source\InvoiceMoment; use Mollie\Payment\Model\Client\Payments\ProcessTransaction; use Mollie\Payment\Service\Mollie\DashboardUrl; +use Mollie\Payment\Service\Mollie\Order\CanRegisterCaptureNotification; use Mollie\Payment\Service\Mollie\Order\LinkTransactionToOrder; use Mollie\Payment\Service\Mollie\TransactionDescription; use Mollie\Payment\Service\Mollie\ValidateMetadata; @@ -132,6 +135,11 @@ class Payments extends AbstractModel */ private $expiredOrderToTransaction; + /** + * @var CanRegisterCaptureNotification + */ + private $canRegisterCaptureNotification; + /** * Payments constructor. * @@ -154,6 +162,7 @@ class Payments extends AbstractModel * @param ValidateMetadata $validateMetadata * @param SaveAdditionalInformationDetails $saveAdditionalInformationDetails * @param ExpiredOrderToTransaction $expiredOrderToTransaction + * @param CanRegisterCaptureNotification $canRegisterCaptureNotification */ public function __construct( OrderRepository $orderRepository, @@ -174,7 +183,8 @@ public function __construct( ProcessTransaction $processTransaction, ValidateMetadata $validateMetadata, SaveAdditionalInformationDetails $saveAdditionalInformationDetails, - ExpiredOrderToTransaction $expiredOrderToTransaction + ExpiredOrderToTransaction $expiredOrderToTransaction, + CanRegisterCaptureNotification $canRegisterCaptureNotification ) { $this->orderRepository = $orderRepository; $this->checkoutSession = $checkoutSession; @@ -195,6 +205,7 @@ public function __construct( $this->validateMetadata = $validateMetadata; $this->saveAdditionalInformationDetails = $saveAdditionalInformationDetails; $this->expiredOrderToTransaction = $expiredOrderToTransaction; + $this->canRegisterCaptureNotification = $canRegisterCaptureNotification; } /** @@ -341,7 +352,7 @@ public function processTransaction(Order $order, $mollieApi, $type = 'webhook', $refunded = isset($paymentData->_links->refunds) ? true : false; - if ($status == 'paid' && !$refunded) { + if (in_array($status, ['paid', 'authorized']) && !$refunded) { $amount = $paymentData->amount->value; $currency = $paymentData->amount->currency; $orderAmount = $this->orderAmount->getByTransactionId($transactionId); @@ -367,7 +378,13 @@ public function processTransaction(Order $order, $mollieApi, $type = 'webhook', $payment->setTransactionId($transactionId); $payment->setCurrencyCode($order->getBaseCurrencyCode()); $payment->setIsTransactionClosed(true); - $payment->registerCaptureNotification($order->getBaseGrandTotal(), true); + + if ($this->canRegisterCaptureNotification->execute($order) || + $type != static::TRANSACTION_TYPE_WEBHOOK + ) { + $payment->registerCaptureNotification($order->getBaseGrandTotal(), true); + } + $order->setState(Order::STATE_PROCESSING); $this->transactionProcessor->process($order, null, $paymentData); diff --git a/Model/Client/Payments/CapturePayment.php b/Model/Client/Payments/CapturePayment.php new file mode 100644 index 00000000000..c7f4fd06ddd --- /dev/null +++ b/Model/Client/Payments/CapturePayment.php @@ -0,0 +1,104 @@ +mollieApiClient = $mollieApiClient; + $this->partialInvoice = $partialInvoice; + $this->mollieHelper = $mollieHelper; + $this->orderRepository = $orderRepository; + $this->invoiceSender = $invoiceSender; + $this->orderCommentHistory = $orderCommentHistory; + $this->shouldEmailInvoice = $shouldEmailInvoice; + } + + public function execute(ShipmentInterface $shipment, OrderInterface $order): void + { + $payment = $order->getPayment(); + $invoice = $this->partialInvoice->createFromShipment($shipment); + if (!$invoice) { + return; + } + + $captureAmount = $invoice->getBaseGrandTotal(); + + $mollieTransactionId = $order->getMollieTransactionId(); + $mollieApi = $this->mollieApiClient->loadByStore($order->getStoreId()); + + $data = []; + if ($captureAmount != $order->getBaseGrandTotal()) { + $data['amount'] = $this->mollieHelper->getAmountArray( + $order->getOrderCurrencyCode(), + $captureAmount + ); + } + + $capture = $mollieApi->paymentCaptures->createForId($mollieTransactionId, $data); + + $payment->setTransactionId($capture->id); + $payment->registerCaptureNotification($captureAmount, true); + + $this->orderRepository->save($order); + + $sendInvoice = $this->shouldEmailInvoice->execute($order->getStoreId(), $payment->getMethod()); + if ($invoice && $invoice->getId() && !$invoice->getEmailSent() && $sendInvoice) { + $this->invoiceSender->send($invoice); + $message = __('Notified customer about invoice #%1', $invoice->getIncrementId()); + $this->orderCommentHistory->add($order, $message, true); + } + } +} diff --git a/Model/Client/Payments/ProcessTransaction.php b/Model/Client/Payments/ProcessTransaction.php index a5be68abbfa..2fb1e0a3e1d 100644 --- a/Model/Client/Payments/ProcessTransaction.php +++ b/Model/Client/Payments/ProcessTransaction.php @@ -73,7 +73,7 @@ public function execute( ); $refunded = isset($molliePayment->_links->refunds) ? true : false; - if ($status == 'paid' && !$refunded) { + if (in_array($status, ['paid', 'authorized']) && !$refunded) { return $this->paymentProcessors->process( 'paid', $magentoOrder, diff --git a/Model/Client/Payments/Processors/SuccessfulPayment.php b/Model/Client/Payments/Processors/SuccessfulPayment.php index cdde164297b..bb98a20c40c 100644 --- a/Model/Client/Payments/Processors/SuccessfulPayment.php +++ b/Model/Client/Payments/Processors/SuccessfulPayment.php @@ -13,8 +13,10 @@ use Mollie\Api\Resources\Payment; use Mollie\Payment\Helper\General; use Mollie\Payment\Model\Client\PaymentProcessorInterface; +use Mollie\Payment\Model\Client\Payments; use Mollie\Payment\Model\Client\ProcessTransactionResponse; use Mollie\Payment\Model\Client\ProcessTransactionResponseFactory; +use Mollie\Payment\Service\Mollie\Order\CanRegisterCaptureNotification; use Mollie\Payment\Service\Order\OrderAmount; use Mollie\Payment\Service\Order\OrderCommentHistory; use Mollie\Payment\Service\Order\SendOrderEmails; @@ -63,6 +65,11 @@ class SuccessfulPayment implements PaymentProcessorInterface */ private $sendOrderEmails; + /** + * @var CanRegisterCaptureNotification + */ + private $canRegisterCaptureNotification; + public function __construct( ProcessTransactionResponseFactory $processTransactionResponseFactory, OrderAmount $orderAmount, @@ -71,7 +78,8 @@ public function __construct( OrderCommentHistory $orderCommentHistory, General $mollieHelper, OrderRepositoryInterface $orderRepository, - SendOrderEmails $sendOrderEmails + SendOrderEmails $sendOrderEmails, + CanRegisterCaptureNotification $canRegisterCaptureNotification ) { $this->processTransactionResponseFactory = $processTransactionResponseFactory; $this->orderAmount = $orderAmount; @@ -81,6 +89,7 @@ public function __construct( $this->mollieHelper = $mollieHelper; $this->orderRepository = $orderRepository; $this->sendOrderEmails = $sendOrderEmails; + $this->canRegisterCaptureNotification = $canRegisterCaptureNotification; } public function process( @@ -118,7 +127,7 @@ public function process( } if (abs($amount - $orderAmount['value']) < 0.01) { - $this->handlePayment($magentoOrder, $molliePayment); + $this->handlePayment($magentoOrder, $molliePayment, $type); } /** @var Order\Invoice|null $invoice */ @@ -138,14 +147,20 @@ public function process( ]); } - private function handlePayment(OrderInterface $magentoOrder, Payment $molliePayment): void + private function handlePayment(OrderInterface $magentoOrder, Payment $molliePayment, string $type): void { /** @var PaymentInterface|Order\Payment $payment */ $payment = $magentoOrder->getPayment(); $payment->setCurrencyCode($magentoOrder->getBaseCurrencyCode()); $payment->setTransactionId($magentoOrder->getMollieTransactionId()); $payment->setIsTransactionClosed(true); - $payment->registerCaptureNotification($magentoOrder->getBaseGrandTotal(), true); + + if ($this->canRegisterCaptureNotification->execute($magentoOrder) || + $type != Payments::TRANSACTION_TYPE_SUBSCRIPTION + ) { + $payment->registerCaptureNotification($magentoOrder->getBaseGrandTotal(), true); + } + $magentoOrder->setState(Order::STATE_PROCESSING); $this->transactionProcessor->process($magentoOrder, null, $molliePayment); diff --git a/Model/Mollie.php b/Model/Mollie.php index a4076cafda8..65f30db0628 100644 --- a/Model/Mollie.php +++ b/Model/Mollie.php @@ -19,6 +19,7 @@ use Magento\Payment\Gateway\Config\ValueHandlerPoolInterface; use Magento\Payment\Gateway\Data\PaymentDataObjectFactory; use Magento\Payment\Gateway\Validator\ValidatorPoolInterface; +use Magento\Payment\Model\InfoInterface; use Magento\Payment\Model\Method\Adapter; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order; @@ -353,7 +354,7 @@ public function processTransactionForOrder(OrderInterface $order, $type = 'webho return $msg; } - $result = $this->orderLockService->execute($order, function (OrderInterface $order) use ( + $output = $this->orderLockService->execute($order, function (OrderInterface $order) use ( $transactionId, $type, $paymentToken @@ -367,9 +368,17 @@ public function processTransactionForOrder(OrderInterface $order, $type = 'webho $order->getPayment()->setAdditionalInformation('mollie_success', $result['success']); - return $result; + // Return the order and the result so we can use this outside this closure. + return [ + 'order' => $order, + 'result' => $result, + ]; }); + // Extract the contents of the closure. + $order = $output['order']; + $result = $output['result']; + $this->eventManager->dispatch('mollie_process_transaction_end', ['order' => $order]); return $result; @@ -421,18 +430,6 @@ public function assignData(DataObject $data) return $this; } - /** - * @param Order\Shipment $shipment - * @param Order $order - * - * @return OrdersApi - * @throws LocalizedException - */ - public function createShipment(Order\Shipment $shipment, Order $order) - { - return $this->ordersApi->createShipment($shipment, $order); - } - /** * @param Order\Shipment $shipment * @param Order\Shipment\Track $track @@ -471,13 +468,13 @@ public function createOrderRefund(Order\Creditmemo $creditmemo, Order $order) } /** - * @param \Magento\Payment\Model\InfoInterface $payment + * @param InfoInterface $payment * @param float $amount * * @return $this * @throws LocalizedException */ - public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount) + public function refund(InfoInterface $payment, $amount): self { /** @var \Magento\Sales\Model\Order $order */ $order = $payment->getOrder(); @@ -508,12 +505,18 @@ public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount) } try { + // The provided $amount is in the currency of the default store. If we are not using the base currency, + // Get the order amount in the currency of the order. + if (!$this->config->useBaseCurrency($order->getStoreId())) { + $amount = $payment->getCreditMemo()->getGrandTotal(); + } + $mollieApi = $this->loadMollieApi($apiKey); $payment = $mollieApi->payments->get($transactionId); $payment->refund([ - "amount" => [ - "currency" => $order->getOrderCurrencyCode(), - "value" => $this->mollieHelper->formatCurrencyValue($amount, $order->getOrderCurrencyCode()) + 'amount' => [ + 'currency' => $order->getOrderCurrencyCode(), + 'value' => $this->mollieHelper->formatCurrencyValue($amount, $order->getOrderCurrencyCode()) ] ]); } catch (\Exception $e) { diff --git a/Observer/SalesOrderShipmentSaveBefore/CreateMollieShipment.php b/Observer/SalesOrderShipmentSaveBefore/CreateMollieShipment.php index 337871bdcde..e9846cb90c3 100644 --- a/Observer/SalesOrderShipmentSaveBefore/CreateMollieShipment.php +++ b/Observer/SalesOrderShipmentSaveBefore/CreateMollieShipment.php @@ -8,6 +8,8 @@ use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Event\Observer; +use Mollie\Payment\Model\Client\Orders; +use Mollie\Payment\Model\Client\Payments\CapturePayment; use Mollie\Payment\Model\Mollie as MollieModel; use Mollie\Payment\Helper\General as MollieHelper; @@ -18,27 +20,29 @@ */ class CreateMollieShipment implements ObserverInterface { - /** - * @var MollieModel - */ - private $mollieModel; /** * @var MollieHelper */ private $mollieHelper; /** - * SalesOrderShipmentAfter constructor. - * - * @param MollieModel $mollieModel - * @param MollieHelper $mollieHelper + * @var Orders */ + private $ordersApi; + + /** + * @var CapturePayment + */ + private $capturePayment; + public function __construct( - MollieModel $mollieModel, - MollieHelper $mollieHelper + MollieHelper $mollieHelper, + Orders $ordersApi, + CapturePayment $capturePayment ) { - $this->mollieModel = $mollieModel; $this->mollieHelper = $mollieHelper; + $this->ordersApi = $ordersApi; + $this->capturePayment = $capturePayment; } /** @@ -54,8 +58,14 @@ public function execute(Observer $observer) /** @var \Magento\Sales\Model\Order $order */ $order = $shipment->getOrder(); - if ($this->mollieHelper->isPaidUsingMollieOrdersApi($order)) { - $this->mollieModel->createShipment($shipment, $order); + $transactionId = $order->getMollieTransactionId() ?? ''; + $useOrdersApi = preg_match('/^ord_\w+$/', $transactionId); + if ($useOrdersApi) { + $this->ordersApi->createShipment($shipment, $order); + } + + if (!$useOrdersApi) { + $this->capturePayment->execute($shipment, $order); } } } diff --git a/Observer/SalesQuotePaymentImportDataBefore/ClearIssuerOnMethodChange.php b/Observer/SalesQuotePaymentImportDataBefore/ClearIssuerOnMethodChange.php new file mode 100644 index 00000000000..e21ee44decd --- /dev/null +++ b/Observer/SalesQuotePaymentImportDataBefore/ClearIssuerOnMethodChange.php @@ -0,0 +1,27 @@ +getData('payment'); + $paymentMethod = $payment->getMethod(); + + /** @var DataObject $input */ + $input = $observer->getData('input'); + $inputMethod = $input->getData('method'); + + if ($paymentMethod != $inputMethod) { + $payment->unsAdditionalInformation('selected_issuer'); + } + } +} diff --git a/Service/Mollie/Order/CanRegisterCaptureNotification.php b/Service/Mollie/Order/CanRegisterCaptureNotification.php new file mode 100644 index 00000000000..553ffbbdb35 --- /dev/null +++ b/Service/Mollie/Order/CanRegisterCaptureNotification.php @@ -0,0 +1,32 @@ +config = $config; + } + + public function execute(OrderInterface $order): bool + { + if ($this->config->useManualCapture($order->getStoreId()) && + $order->getPayment()->getMethod() == Creditcard::CODE + ) { + return false; + } + + return true; + } +} diff --git a/Service/Mollie/Order/CreateInvoiceOnShipment.php b/Service/Mollie/Order/CreateInvoiceOnShipment.php new file mode 100644 index 00000000000..79c4867cc8f --- /dev/null +++ b/Service/Mollie/Order/CreateInvoiceOnShipment.php @@ -0,0 +1,45 @@ +config = $config; + } + + public function execute(OrderInterface $order): bool + { + $methodCode = $order->getPayment()->getMethod(); + if (in_array($methodCode, [ + 'mollie_methods_billie', + 'mollie_methods_klarnapaylater', + 'mollie_methods_klarnapaynow', + 'mollie_methods_klarnasliceit', + 'mollie_methods_in3', + ])) { + return true; + } + + $transactionId = $order->getMollieTransactionId() ?? ''; + $api = preg_match('/^ord_\w+$/', $transactionId) ? 'orders' : 'payments'; + if ($methodCode == 'mollie_methods_creditcard' && + $this->config->useManualCapture($order->getStoreId()) && + $api == 'payments' + ) { + return true; + } + + return false; + } +} diff --git a/Service/Order/Invoice/ShouldEmailInvoice.php b/Service/Order/Invoice/ShouldEmailInvoice.php new file mode 100644 index 00000000000..445acc5829d --- /dev/null +++ b/Service/Order/Invoice/ShouldEmailInvoice.php @@ -0,0 +1,47 @@ +config = $config; + } + + public function execute(int $storeId, string $paymentMethod): bool + { + if (!$this->config->sendInvoiceEmail($storeId)) { + return false; + } + + if (!$this->isKlarna($paymentMethod)) { + return true; + } + + return $this->config->sendInvoiceEmailForKlarna($storeId); + } + + private function isKlarna(string $paymentMethod) + { + return in_array( + $paymentMethod, + [ + 'mollie_methods_klarna', + 'mollie_methods_klarnapaylater', + 'mollie_methods_klarnapaynow', + 'mollie_methods_klarnasliceit', + ] + ); + } +} diff --git a/Service/Order/PartialInvoice.php b/Service/Order/PartialInvoice.php index c2846dea036..64fc97c3aee 100644 --- a/Service/Order/PartialInvoice.php +++ b/Service/Order/PartialInvoice.php @@ -10,6 +10,7 @@ use Magento\Sales\Model\Service\InvoiceService; use Mollie\Payment\Helper\General as MollieHelper; use Mollie\Payment\Model\Adminhtml\Source\InvoiceMoment; +use Mollie\Payment\Service\Mollie\Order\CreateInvoiceOnShipment; class PartialInvoice { @@ -18,6 +19,11 @@ class PartialInvoice */ private $mollieHelper; + /** + * @var CreateInvoiceOnShipment + */ + private $createInvoiceOnShipment; + /** * @var InvoiceService */ @@ -30,32 +36,22 @@ class PartialInvoice public function __construct( MollieHelper $mollieHelper, + CreateInvoiceOnShipment $createInvoiceOnShipment, InvoiceService $invoiceService, InvoiceRepository $invoiceRepository ) { $this->mollieHelper = $mollieHelper; $this->invoiceService = $invoiceService; $this->invoiceRepository = $invoiceRepository; + $this->createInvoiceOnShipment = $createInvoiceOnShipment; } public function createFromShipment(ShipmentInterface $shipment) { /** @var OrderInterface $order */ $order = $shipment->getOrder(); - $payment = $order->getPayment(); - if (!in_array( - $payment->getMethod(), - [ - 'mollie_methods_billie', - 'mollie_methods_klarnapaylater', - 'mollie_methods_klarnapaynow', - 'mollie_methods_klarnasliceit', - 'mollie_methods_in3', - ] - ) || - $this->mollieHelper->getInvoiceMoment($order->getStoreId()) != InvoiceMoment::ON_SHIPMENT - ) { + if (!$this->createInvoiceOnShipment->execute($order)) { return null; } diff --git a/Service/Order/Reorder.php b/Service/Order/Reorder.php index fc02bc067cb..1526a159307 100644 --- a/Service/Order/Reorder.php +++ b/Service/Order/Reorder.php @@ -20,6 +20,7 @@ use Mollie\Payment\Config; use Mollie\Payment\Model\Adminhtml\Source\SecondChancePaymentMethod; use Mollie\Payment\Plugin\InventorySales\Model\IsProductSalableForRequestedQtyCondition\IsSalableWithReservationsCondition\DisableCheckForAdminOrders; +use Mollie\Payment\Service\Order\Invoice\ShouldEmailInvoice; class Reorder { @@ -73,6 +74,11 @@ class Reorder */ private $disableCheckForAdminOrders; + /** + * @var ShouldEmailInvoice + */ + private $shouldEmailInvoice; + public function __construct( Config $config, Create $orderCreate, @@ -82,7 +88,8 @@ public function __construct( TransactionFactory $transactionFactory, Session $checkoutSession, Product $productHelper, - DisableCheckForAdminOrders $disableCheckForAdminOrders + DisableCheckForAdminOrders $disableCheckForAdminOrders, + ShouldEmailInvoice $shouldEmailInvoice ) { $this->config = $config; $this->orderCreate = $orderCreate; @@ -93,6 +100,7 @@ public function __construct( $this->checkoutSession = $checkoutSession; $this->productHelper = $productHelper; $this->disableCheckForAdminOrders = $disableCheckForAdminOrders; + $this->shouldEmailInvoice = $shouldEmailInvoice; } public function create(OrderInterface $originalOrder): OrderInterface @@ -195,7 +203,9 @@ private function createInvoiceFor(OrderInterface $order) private function sendInvoice(InvoiceInterface $invoice, OrderInterface $order) { /** @var Order\Invoice $invoice */ - if ($invoice->getEmailSent() || !$this->config->sendInvoiceEmail($invoice->getStoreId())) { + if ($invoice->getEmailSent() || + !$this->shouldEmailInvoice->execute((int)$order->getStoreId(), $order->getPayment()->getMethod()) + ) { return; } diff --git a/Service/Order/SendOrderEmails.php b/Service/Order/SendOrderEmails.php index 5f13ff9f30c..225197e192f 100644 --- a/Service/Order/SendOrderEmails.php +++ b/Service/Order/SendOrderEmails.php @@ -11,7 +11,7 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; use Magento\Sales\Model\Order\Email\Sender\OrderSender; -use Mollie\Payment\Helper\General; +use Mollie\Payment\Service\Order\Invoice\ShouldEmailInvoice; class SendOrderEmails { @@ -25,11 +25,6 @@ class SendOrderEmails */ private $disableInvoiceSending = false; - /** - * @var General - */ - private $mollieHelper; - /** * @var OrderSender */ @@ -45,16 +40,21 @@ class SendOrderEmails */ private $invoiceSender; + /** + * @var ShouldEmailInvoice + */ + private $shouldEmailInvoice; + public function __construct( - General $mollieHelper, OrderSender $orderSender, OrderCommentHistory $orderCommentHistory, - InvoiceSender $invoiceSender + InvoiceSender $invoiceSender, + ShouldEmailInvoice $shouldEmailInvoice ) { - $this->mollieHelper = $mollieHelper; $this->orderSender = $orderSender; $this->orderCommentHistory = $orderCommentHistory; $this->invoiceSender = $invoiceSender; + $this->shouldEmailInvoice = $shouldEmailInvoice; } public function disableOrderConfirmationSending(): void @@ -88,8 +88,11 @@ public function disableInvoiceSending(): void public function sendInvoiceEmail(InvoiceInterface $invoice): void { + $order = $invoice->getOrder(); + $paymentMethod = $order->getPayment()->getMethod(); + if ($invoice->getEmailSent() || - !$this->mollieHelper->sendInvoice($invoice->getStoreId()) || + !$this->shouldEmailInvoice->execute((int)$invoice->getStoreId(), $paymentMethod) || $this->disableInvoiceSending ) { return; @@ -98,10 +101,10 @@ public function sendInvoiceEmail(InvoiceInterface $invoice): void try { $this->invoiceSender->send($invoice); $message = __('Notified customer about invoice #%1', $invoice->getIncrementId()); - $this->orderCommentHistory->add($invoice->getOrder(), $message, true); + $this->orderCommentHistory->add($order, $message, true); } catch (\Throwable $exception) { $message = __('Unable to send the invoice: %1', $exception->getMessage()); - $this->orderCommentHistory->add($invoice->getOrder(), $message, true); + $this->orderCommentHistory->add($order, $message, true); } } } diff --git a/Service/Order/TransactionPart/CaptureMode.php b/Service/Order/TransactionPart/CaptureMode.php new file mode 100644 index 00000000000..e66c1ce7c0b --- /dev/null +++ b/Service/Order/TransactionPart/CaptureMode.php @@ -0,0 +1,40 @@ +config = $config; + } + + public function process(OrderInterface $order, $apiMethod, array $transaction) + { + if ($apiMethod == Orders::CHECKOUT_TYPE) { + return $transaction; + } + + if ($order->getPayment()->getMethod() != 'mollie_methods_creditcard' || + !$this->config->useManualCapture($order->getStoreId()) + ) { + return $transaction; + } + + $transaction['captureMode'] = 'manual'; + + return $transaction; + } +} diff --git a/Service/PaymentFee/Types/Percentage.php b/Service/PaymentFee/Types/Percentage.php index 79b54616bc6..603f4913234 100644 --- a/Service/PaymentFee/Types/Percentage.php +++ b/Service/PaymentFee/Types/Percentage.php @@ -47,8 +47,8 @@ public function calculate(CartInterface $cart, Total $total) { $percentage = $this->config->getPercentage($cart->getPayment()->getMethod(), $cart->getStoreId()); - $shipping = $total->getShippingInclTax(); - $subtotal = $total->getData('subtotal_incl_tax'); + $shipping = $total->getBaseShippingInclTax(); + $subtotal = $total->getData('base_subtotal_incl_tax'); if (!$subtotal) { $subtotal = $total->getTotalAmount('subtotal'); } diff --git a/Test/End-2-end/cypress/e2e/magento/backend/manual-capture.cy.js b/Test/End-2-end/cypress/e2e/magento/backend/manual-capture.cy.js new file mode 100644 index 00000000000..cb513a74edd --- /dev/null +++ b/Test/End-2-end/cypress/e2e/magento/backend/manual-capture.cy.js @@ -0,0 +1,94 @@ +import Configuration from 'Actions/backend/Configuration'; +import CheckoutPaymentPage from "Pages/frontend/CheckoutPaymentPage"; +import VisitCheckoutPaymentCompositeAction from "CompositeActions/VisitCheckoutPaymentCompositeAction"; +import ComponentsAction from "Actions/checkout/ComponentsAction"; +import MollieHostedPaymentPage from "Pages/mollie/MollieHostedPaymentPage"; +import CheckoutSuccessPage from "Pages/frontend/CheckoutSuccessPage"; +import OrdersPage from "Pages/backend/OrdersPage"; +import ShipmentPage from "Pages/backend/ShipmentPage"; + +const configuration = new Configuration(); +const checkoutPaymentPage = new CheckoutPaymentPage(); +const visitCheckoutPayment = new VisitCheckoutPaymentCompositeAction(); +const components = new ComponentsAction(); +const mollieHostedPaymentPage = new MollieHostedPaymentPage(); +const checkoutSuccessPage = new CheckoutSuccessPage(); +const ordersPage = new OrdersPage(); +const shipmentPage = new ShipmentPage(); + +describe('Manual capture works as expected', () => { + after(() => { + cy.backendLogin(); + + // Make sure to set this back to No to not influence other tests + configuration.setValue('Advanced', 'Triggers & Languages', 'Manual Capture', 'No'); + }); + + it('C1064183: Validate that with manual capture enabled the invoice is created when placing the order', () => { + configuration.setValue('Advanced', 'Triggers & Languages', 'Manual Capture', 'No'); + + visitCheckoutPayment.visit(); + + checkoutPaymentPage.selectPaymentMethod('Credit Card'); + + components.fillComponentsForm( + 'Mollie Tester', + '3782 822463 10005', + '1230', + '1234' + ); + + checkoutPaymentPage.placeOrder(); + + mollieHostedPaymentPage.selectStatus('paid'); + + checkoutSuccessPage.assertThatOrderSuccessPageIsShown(); + + cy.backendLogin(); + + cy.get('@order-id').then((orderId) => { + ordersPage.openOrderById(orderId); + }); + + ordersPage.assertOrderStatusIs('Processing'); + + ordersPage.assertOrderHasInvoice(); + }); + + it('C1064182: Validate that with manual capture enabled the invoice is created when a shipment is created', () => { + configuration.setValue('Advanced', 'Triggers & Languages', 'Manual Capture', 'Yes'); + + visitCheckoutPayment.visit(); + + checkoutPaymentPage.selectPaymentMethod('Credit Card'); + + components.fillComponentsForm( + 'Mollie Tester', + '3782 822463 10005', + '1230', + '1234' + ); + + checkoutPaymentPage.placeOrder(); + + mollieHostedPaymentPage.selectStatus('authorized'); + + checkoutSuccessPage.assertThatOrderSuccessPageIsShown(); + + cy.backendLogin(); + + cy.get('@order-id').then((orderId) => { + ordersPage.openOrderById(orderId); + }); + + ordersPage.assertOrderStatusIs('Processing'); + + ordersPage.assertOrderHasNoInvoices(); + + ordersPage.ship(); + + shipmentPage.ship(); + + ordersPage.assertOrderHasInvoice(); + }); +}); diff --git a/Test/End-2-end/cypress/support/actions/backend/Configuration.js b/Test/End-2-end/cypress/support/actions/backend/Configuration.js new file mode 100644 index 00000000000..6447f6111dc --- /dev/null +++ b/Test/End-2-end/cypress/support/actions/backend/Configuration.js @@ -0,0 +1,34 @@ +export default class Configuration { + setValue(section, group, field, value) { + cy.backendLogin(); + + cy.get('[data-ui-id="menu-magento-config-system-config"] > a').then(element => { + cy.visit(element.attr('href')); + }); + + cy.contains('Currency Setup').should('be.visible'); + + // When this is not visible, the page is not loaded yet. + cy.get('#system_config_tabs .mollie-tab').click(); + + cy.get('.mollie-tab').contains(section).click(); + + // Wait for JS to load + cy.get('.mollie-tab').should('have.class', '_show'); + + cy.get('.entry-edit').contains(group).then(element => { + if (!element.hasClass('open')) { + cy.get(element).click(); + } + }); + + cy.get('label').contains(field).parents('tr').find(':input').select(value); + + cy.get('#save').click(); + + // Wait for JS to load + cy.get('.mollie-tab').should('have.class', '_show'); + + cy.contains('You saved the configuration.'); + } +} diff --git a/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js b/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js index f1432b1cb72..5df55045e85 100644 --- a/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js +++ b/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js @@ -7,10 +7,10 @@ const checkoutPage = new CheckoutPage(); const checkoutShippingPage = new CheckoutShippingPage(); export default class VisitCheckoutPaymentCompositeAction { - visit(fixture = 'NL') { + visit(fixture = 'NL', quantity = 1) { productPage.openProduct(Cypress.env('defaultProductId')); - productPage.addSimpleProductToCart(); + productPage.addSimpleProductToCart(quantity); checkoutPage.visit(); diff --git a/Test/End-2-end/cypress/support/commands.js b/Test/End-2-end/cypress/support/commands.js index 4c48fc33f31..21a6875fcbe 100644 --- a/Test/End-2-end/cypress/support/commands.js +++ b/Test/End-2-end/cypress/support/commands.js @@ -19,6 +19,8 @@ Cypress.Commands.add('backendLogin', () => { cy.url().should('include', '/admin/admin/dashboard'); }); + + cy.visit('/admin/admin/dashboard'); }); Cypress.Commands.add('getIframeBody', (selector) => { diff --git a/Test/End-2-end/cypress/support/pages/backend/OrdersPage.js b/Test/End-2-end/cypress/support/pages/backend/OrdersPage.js index b8df2cc6c2c..a1044ce34f8 100644 --- a/Test/End-2-end/cypress/support/pages/backend/OrdersPage.js +++ b/Test/End-2-end/cypress/support/pages/backend/OrdersPage.js @@ -28,4 +28,28 @@ export default class OrdersPage { assertMolliePaymentStatusIs(status) { cy.get('.mollie-payment-status').contains(status); } + + assertOrderHasInvoice() { + cy.get('#sales_order_view_tabs_order_invoices').click(); + + // Can be really slow + cy.get('.spinner').should('not.be.visible', {timeout: 30000}); + + cy.get('#sales_order_view_tabs_order_invoices_content tbody').should('be.visible'); + cy.get('#sales_order_view_tabs_order_invoices_content').should('contain', '1 records found'); + } + + assertOrderHasNoInvoices() { + cy.get('#sales_order_view_tabs_order_invoices').click(); + + // Can be really slow + cy.get('.spinner').should('not.be.visible', {timeout: 30000}); + + cy.get('#sales_order_view_tabs_order_invoices_content tbody').should('be.visible'); + cy.get('#sales_order_view_tabs_order_invoices_content').should('contain', '0 records found'); + } + + ship() { + cy.get('#order_ship').should('be.enabled').click(); + } } diff --git a/Test/End-2-end/cypress/support/pages/backend/ShipmentPage.js b/Test/End-2-end/cypress/support/pages/backend/ShipmentPage.js new file mode 100644 index 00000000000..9578deb7c99 --- /dev/null +++ b/Test/End-2-end/cypress/support/pages/backend/ShipmentPage.js @@ -0,0 +1,14 @@ +export default class ShipmentPage { + + changeQuantity(sku, quantity) { + cy.get('.product-sku-block').contains(sku).parents('tr').find('.col-qty input').clear().type(quantity); + cy.get('#shipment_comment_text').focus(); // This is needed to trigger the change event + } + + ship() { + cy.get('#system_messages').should('have.length.gte', 0); + cy.get('[data-ui-id="order-items-submit-button"]').should('be.enabled').click(); + + cy.get('[data-ui-id="sales-order-tabs-tab-sales-order-view-tabs"] .ui-state-active').should('be.visible'); + } +} diff --git a/Test/End-2-end/cypress/support/pages/frontend/ProductPage.js b/Test/End-2-end/cypress/support/pages/frontend/ProductPage.js index 49ce6dbdf44..238fb0d5207 100644 --- a/Test/End-2-end/cypress/support/pages/frontend/ProductPage.js +++ b/Test/End-2-end/cypress/support/pages/frontend/ProductPage.js @@ -6,10 +6,13 @@ export default class ProductPage { cy.visit('/catalog/product/view/id/' + productId); } - addSimpleProductToCart() { + addSimpleProductToCart(quantity = 1) { + cy.get('#qty').clear().type(quantity); + + cy.get('#search').focus(); + cy.get('.action.tocart.primary').should('be.enabled').click(); - // TODO: Make this dynamic - cy.get('.counter.qty').should('contain', 1); + cy.get('.counter.qty').should('contain', quantity); } } diff --git a/Test/Fakes/Service/Mollie/FakeMollieApiClient.php b/Test/Fakes/Service/Mollie/FakeMollieApiClient.php index a79995a64e8..dbe309a4376 100644 --- a/Test/Fakes/Service/Mollie/FakeMollieApiClient.php +++ b/Test/Fakes/Service/Mollie/FakeMollieApiClient.php @@ -6,6 +6,8 @@ namespace Mollie\Payment\Test\Fakes\Service\Mollie; +use Magento\TestFramework\ObjectManager; +use Mollie\Api\Resources\Payment; use Mollie\Payment\Service\Mollie\MollieApiClient; class FakeMollieApiClient extends MollieApiClient @@ -20,6 +22,13 @@ public function setInstance(\Mollie\Api\MollieApiClient $instance) $this->instance = $instance; } + private function loadInstance(): void + { + if (!$this->instance) { + $this->instance = parent::loadByStore(); + } + } + public function loadByStore(int $storeId = null): \Mollie\Api\MollieApiClient { if ($this->instance) { @@ -28,4 +37,20 @@ public function loadByStore(int $storeId = null): \Mollie\Api\MollieApiClient return parent::loadByStore($storeId); } + + public function returnFakePayment(Payment $payment = null): ?Payment + { + $this->loadInstance(); + + $endpoint = ObjectManager::getInstance()->create(FakeMolliePaymentApiEndpoint::class); + + $this->instance->payments = $endpoint; + + if ($payment) { + $endpoint->setFakePayment($payment); + return $payment; + } + + return null; + } } diff --git a/Test/Fakes/Service/Mollie/FakeMolliePaymentApiEndpoint.php b/Test/Fakes/Service/Mollie/FakeMolliePaymentApiEndpoint.php new file mode 100644 index 00000000000..f3165768a60 --- /dev/null +++ b/Test/Fakes/Service/Mollie/FakeMolliePaymentApiEndpoint.php @@ -0,0 +1,28 @@ +fakePayment = $payment; + } + + public function get($paymentId, array $parameters = []) + { + if (!$this->fakePayment) { + throw new \Exception('No fake payment set. Aborting'); + } + + return $this->fakePayment; + } +} diff --git a/Test/Integration/Model/Client/ProcessTransactionTest.php b/Test/Integration/Model/Client/ProcessTransactionTest.php index e2cc2daa3da..ace9585fb8f 100644 --- a/Test/Integration/Model/Client/ProcessTransactionTest.php +++ b/Test/Integration/Model/Client/ProcessTransactionTest.php @@ -153,6 +153,7 @@ public function callsTheCorrectProcessorProvider(): array { return [ [OrderStatus::STATUS_CREATED, 'created'], + [OrderStatus::STATUS_SHIPPING, 'shipping'], ]; } diff --git a/Test/Integration/Model/MollieTest.php b/Test/Integration/Model/MollieTest.php index 1e03627d7ec..ade576fc57d 100644 --- a/Test/Integration/Model/MollieTest.php +++ b/Test/Integration/Model/MollieTest.php @@ -6,18 +6,21 @@ use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Quote\Model\Quote; +use Magento\Sales\Api\Data\CreditmemoInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Mollie\Api\Endpoints\MethodEndpoint; use Mollie\Api\Exceptions\ApiException; use Mollie\Api\MollieApiClient; +use Mollie\Api\Resources\Payment; use Mollie\Payment\Helper\General; use Mollie\Payment\Model\Client\Orders; use Mollie\Payment\Model\Client\Payments; use Mollie\Payment\Model\Methods\Ideal; use Mollie\Payment\Model\Mollie; use Mollie\Payment\Test\Fakes\Model\Client\Orders\ProcessTransactionFake; +use Mollie\Payment\Test\Fakes\Service\Mollie\FakeMollieApiClient; use Mollie\Payment\Test\Integration\IntegrationTestCase; class MollieTest extends IntegrationTestCase @@ -289,6 +292,45 @@ public function testIsNotAvailableForLongSteetnames(): void $this->assertFalse($instance->isAvailable($quote)); } + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_general/currency 0 + * @magentoConfigFixture default_store payment/mollie_general/type test + * @magentoConfigFixture default_store payment/mollie_general/apikey_test test_dummyapikeywhichmustbe30characterslong + * + * @return void + * @throws LocalizedException + */ + public function testRefundsInTheCorrectAmount(): void + { + $order = $this->loadOrder('100000001'); + $order->setMollieTransactionId('tr_12345'); + + $paymentMock = $this->createMock(Payment::class); + $paymentMock->method('refund')->with($this->callback(function ($parameters) { + $this->assertEquals(56.78, $parameters['amount']['value']); + + return true; + })); + + $client = $this->loadFakeMollieApiClient(); + $client->returnFakePayment($paymentMock); + + /** @var Mollie $instance */ + $instance = $this->objectManager->create(Mollie::class); + + /** @var $infoPayment $infoPayment */ + $infoPayment = $this->objectManager->get(\Magento\Sales\Model\Order\Payment::class); + $infoPayment->setOrder($order); + + $creditmemo = $this->objectManager->create(CreditmemoInterface::class); + $creditmemo->setBaseGrandTotal(12.34); + $creditmemo->setGrandTotal(56.78); + $infoPayment->setCreditmemo($creditmemo); + + $instance->refund($infoPayment, 12.34); + } + /** * @magentoConfigFixture default_store payment/mollie_general/enabled 1 * @magentoConfigFixture default_store payment/mollie_methods_ideal/active 1 diff --git a/Test/Integration/Observer/SalesQuotePaymentImportDataBefore/ClearIssuerOnMethodChangeTest.php b/Test/Integration/Observer/SalesQuotePaymentImportDataBefore/ClearIssuerOnMethodChangeTest.php new file mode 100644 index 00000000000..015fa6a6294 --- /dev/null +++ b/Test/Integration/Observer/SalesQuotePaymentImportDataBefore/ClearIssuerOnMethodChangeTest.php @@ -0,0 +1,56 @@ +objectManager->create(PaymentInterface::class); + $payment->setMethod('mollie_methods_ideal'); + + $payment->setAdditionalInformation('selected_issuer', 'test_issuer'); + + $data = new DataObject(); + $data->setData('method', 'mollie_methods_giftcard'); + + $observer = $this->objectManager->create(Observer::class); + $observer->setData('payment', $payment); + $observer->setData('input', $data); + + $instance = $this->objectManager->create(ClearIssuerOnMethodChange::class); + $instance->execute($observer); + + $this->assertNull($payment->getAdditionalInformation('selected_issuer')); + } + + public function testDoesNothingWhenTheMethodIsNotChanged(): void + { + /** \Magento\Quote\Api\Data\PaymentInterface $payment */ + $payment = $this->objectManager->create(PaymentInterface::class); + $payment->setMethod('mollie_methods_ideal'); + + $payment->setAdditionalInformation('selected_issuer', 'test_issuer'); + + $data = new DataObject(); + $data->setData('method', 'mollie_methods_ideal'); + + $observer = $this->objectManager->create(Observer::class); + $observer->setData('payment', $payment); + $observer->setData('input', $data); + + $instance = $this->objectManager->create(ClearIssuerOnMethodChange::class); + $instance->execute($observer); + + $this->assertEquals('test_issuer', $payment->getAdditionalInformation('selected_issuer')); + } +} diff --git a/Test/Integration/PHPUnit/IntegrationTestCaseVersion9AndHigher.php b/Test/Integration/PHPUnit/IntegrationTestCaseVersion9AndHigher.php index f81f8a5f779..6e34ae46eb6 100644 --- a/Test/Integration/PHPUnit/IntegrationTestCaseVersion9AndHigher.php +++ b/Test/Integration/PHPUnit/IntegrationTestCaseVersion9AndHigher.php @@ -12,6 +12,8 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\Annotation\DataFixture; use Magento\TestFramework\ObjectManager; +use Mollie\Payment\Service\Mollie\MollieApiClient; +use Mollie\Payment\Test\Fakes\Service\Mollie\FakeMollieApiClient; use Mollie\Payment\Test\Integration\PHPUnit\IntegrationTestCaseTrait; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use PHPUnit\Framework\TestCase; @@ -112,4 +114,13 @@ protected function loadOrder($incrementId) return array_shift($orderList); } + + public function loadFakeMollieApiClient(): FakeMollieApiClient + { + $client = $this->objectManager->create(FakeMollieApiClient::class); + + $this->objectManager->addSharedInstance($client, MollieApiClient::class); + + return $client; + } } diff --git a/Test/Integration/Service/Mollie/Order/CanRegisterCaptureNotificationTest.php b/Test/Integration/Service/Mollie/Order/CanRegisterCaptureNotificationTest.php new file mode 100644 index 00000000000..bb2e92e2851 --- /dev/null +++ b/Test/Integration/Service/Mollie/Order/CanRegisterCaptureNotificationTest.php @@ -0,0 +1,56 @@ +loadOrderById('100000001'); + + /** @var CanRegisterCaptureNotification $instance */ + $instance = $this->objectManager->create(CanRegisterCaptureNotification::class); + + $this->assertTrue($instance->execute($order)); + } + + /** + * @magentoConfigFixture default_store payment/mollie_general/enable_manual_capture 0 + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + */ + public function testCanCaptureWhenEnabledButNotCreditcard(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_ideal'); + + /** @var CanRegisterCaptureNotification $instance */ + $instance = $this->objectManager->create(CanRegisterCaptureNotification::class); + + $this->assertTrue($instance->execute($order)); + } + + /** + * @magentoConfigFixture default_store payment/mollie_general/enable_manual_capture 1 + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + */ + public function testCannotCaptureWhenEnabledAndCreditcard(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_creditcard'); + + /** @var CanRegisterCaptureNotification $instance */ + $instance = $this->objectManager->create(CanRegisterCaptureNotification::class); + + $this->assertFalse($instance->execute($order)); + } +} diff --git a/Test/Integration/Service/Mollie/Order/CreateInvoiceOnShipmentTest.php b/Test/Integration/Service/Mollie/Order/CreateInvoiceOnShipmentTest.php new file mode 100644 index 00000000000..3dccca7fc3d --- /dev/null +++ b/Test/Integration/Service/Mollie/Order/CreateInvoiceOnShipmentTest.php @@ -0,0 +1,112 @@ +loadOrderById('100000001'); + $order->getPayment()->setMethod($method); + $order->setMollieTransactionId('ord_1234567890'); + + /** @var CreateInvoiceOnShipment $instance */ + $instance = $this->objectManager->create(CreateInvoiceOnShipment::class); + + $this->assertTrue($instance->execute($order)); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_methods_creditcard/method payment + * @magentoConfigFixture default_store payment/mollie_general/enable_manual_capture 1 + * @return void + */ + public function testIsEnabledWhenCreatePaymentAuthorizationIsEnabledAndApiIsPayments(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_creditcard'); + $order->setMollieTransactionId('tr_1234567890'); + + /** @var CreateInvoiceOnShipment $instance */ + $instance = $this->objectManager->create(CreateInvoiceOnShipment::class); + + $this->assertTrue($instance->execute($order)); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_methods_creditcard/method order + * @magentoConfigFixture default_store payment/mollie_general/enable_manual_capture 1 + * @return void + */ + public function testIsDisabledWhenCreatePaymentAuthorizationIsEnabledAndApiIsOrders(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_creditcard'); + $order->setMollieTransactionId('ord_1234567890'); + + /** @var CreateInvoiceOnShipment $instance */ + $instance = $this->objectManager->create(CreateInvoiceOnShipment::class); + + $this->assertFalse($instance->execute($order)); + } + + /** + * @dataProvider isDisabledWhenTheMethodDoesNotSupportThisProvider + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_methods_creditcard/method order + * @magentoConfigFixture default_store payment/mollie_general/enable_manual_capture 1 + * @return void + */ + public function testIsDisabledWhenTheMethodDoesNotSupportThis(string $method): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod($method); + $order->setMollieTransactionId('tr_1234567890'); + + /** @var CreateInvoiceOnShipment $instance */ + $instance = $this->objectManager->create(CreateInvoiceOnShipment::class); + + $this->assertFalse($instance->execute($order)); + } + + public function isEnabledByMethodProvider(): array + { + return [ + [Klarnapaylater::CODE], + [Klarnapaynow::CODE], + [Klarnasliceit::CODE], + [Billie::CODE], + [In3::CODE], + ]; + } + + public function isDisabledWhenTheMethodDoesNotSupportThisProvider(): array + { + return [ + [Bancontact::CODE], + [Banktransfer::CODE], + [Ideal::CODE], + ]; + } +} diff --git a/Test/Integration/Service/Order/PartialInvoiceTest.php b/Test/Integration/Service/Order/PartialInvoiceTest.php index 680b5eaebbd..ddda8a6ad1e 100644 --- a/Test/Integration/Service/Order/PartialInvoiceTest.php +++ b/Test/Integration/Service/Order/PartialInvoiceTest.php @@ -20,7 +20,10 @@ public function testDoesNotCreateInvoiceWhenWrongPaymentMethod() /** @var PartialInvoice $instance */ $instance = $this->objectManager->get(PartialInvoice::class); - $this->assertNull($instance->createFromShipment($shipments->getFirstItem())); + $shipment = $shipments->getFirstItem(); + $shipment->getOrder()->setMollieTransactionId('ord_abc123')->save(); + + $this->assertNull($instance->createFromShipment($shipment)); } /** diff --git a/Test/Integration/Service/Order/TransactionPart/CaptureModeTest.php b/Test/Integration/Service/Order/TransactionPart/CaptureModeTest.php new file mode 100644 index 00000000000..b6e18f99447 --- /dev/null +++ b/Test/Integration/Service/Order/TransactionPart/CaptureModeTest.php @@ -0,0 +1,84 @@ +loadOrderById('100000001'); + $order->getPayment()->setMethod(Creditcard::CODE); + + /** @var CaptureMode $instance */ + $instance = $this->objectManager->create(CaptureMode::class); + + $transaction = $instance->process($order, Orders::CHECKOUT_TYPE, []); + + $this->assertArrayNotHasKey('captureMode', $transaction); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + */ + public function testDoesNothingWhenThePaymentMethodIsNotCreditcard(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod(Ideal::CODE); + + /** @var CaptureMode $instance */ + $instance = $this->objectManager->create(CaptureMode::class); + + $transaction = $instance->process($order, Payments::CHECKOUT_TYPE, []); + + $this->assertArrayNotHasKey('captureMode', $transaction); + } + + /** + * @magentoConfigFixture default_store payment/mollie_general/enable_manual_capture 0 + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testDoesNothingWhenNotEnabled(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod(Creditcard::CODE); + + /** @var CaptureMode $instance */ + $instance = $this->objectManager->create(CaptureMode::class); + + $transaction = $instance->process($order, Payments::CHECKOUT_TYPE, []); + + $this->assertArrayNotHasKey('captureMode', $transaction); + } + + /** + * @magentoConfigFixture default_store payment/mollie_general/enable_manual_capture 1 + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + */ + public function testSetsTheModeWhenApplicable(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod(Creditcard::CODE); + + /** @var CaptureMode $instance */ + $instance = $this->objectManager->create(CaptureMode::class); + + $transaction = $instance->process($order, Payments::CHECKOUT_TYPE, []); + + $this->assertArrayHasKey('captureMode', $transaction); + } +} diff --git a/Test/Integration/Service/PaymentFee/Types/PercentageTest.php b/Test/Integration/Service/PaymentFee/Types/PercentageTest.php new file mode 100644 index 00000000000..d232d9c9436 --- /dev/null +++ b/Test/Integration/Service/PaymentFee/Types/PercentageTest.php @@ -0,0 +1,43 @@ +objectManager->create(Percentage::class); + + $cart = $this->objectManager->create(Quote::class); + $cart->load('test01', 'reserved_order_id'); + $cart->getPayment()->setMethod('mollie_methods_ideal'); + + /** @var Total $total */ + $total = $this->objectManager->create(Total::class); + + $total->setBaseShippingInclTax(10); + $total->setData('base_subtotal_incl_tax', 10); + + $total->setShippingInclTax(999); // Intentionally set to a high value to make sure it's not used + $total->setData('subtotal_incl_tax', 999); + + $result = $instance->calculate($cart, $total); + + $this->assertEquals(0.2, $result->getAmount()); + } +} diff --git a/Test/Mollie/Payment/Test/Integration/Service/Order/Invoice/ShouldEmailInvoiceTest.php b/Test/Mollie/Payment/Test/Integration/Service/Order/Invoice/ShouldEmailInvoiceTest.php new file mode 100644 index 00000000000..7c852eb2428 --- /dev/null +++ b/Test/Mollie/Payment/Test/Integration/Service/Order/Invoice/ShouldEmailInvoiceTest.php @@ -0,0 +1,73 @@ +objectManager->create(ShouldEmailInvoice::class); + + $result = $instance->execute(1, 'mollie_methods_ideal'); + + $this->assertFalse($result); + } + + /** + * @magentoConfigFixture default_store payment/mollie_general/invoice_notify 1 + * + * @return void + */ + public function testShouldReturnTrueWhenInvoiceSendingEnabledButNowKlarna(): void + { + /** @var ShouldEmailInvoice $instance */ + $instance = $this->objectManager->create(ShouldEmailInvoice::class); + + $result = $instance->execute(1, 'mollie_methods_ideal'); + + $this->assertTrue($result); + } + + /** + * @magentoConfigFixture default_store payment/mollie_general/invoice_notify 1 + * @magentoConfigFixture default_store payment/mollie_general/invoice_notify_klarna 1 + * + * @return void + */ + public function testShouldReturnTrueWhenEnabledAndMethodIsKlarnaAndKlarnaInvoiceIsEnabled(): void + { + /** @var ShouldEmailInvoice $instance */ + $instance = $this->objectManager->create(ShouldEmailInvoice::class); + + $result = $instance->execute(1, 'mollie_methods_klarnapaynow'); + + $this->assertTrue($result); + } + + /** + * @magentoConfigFixture default_store payment/mollie_general/invoice_notify 1 + * @magentoConfigFixture default_store payment/mollie_general/invoice_notify_klarna 0 + * + * @return void + */ + public function testShouldReturnFalseWhenEnabledAndMethodIsKlarnaAndKlarnaInvoiceIsDisabled(): void + { + /** @var ShouldEmailInvoice $instance */ + $instance = $this->objectManager->create(ShouldEmailInvoice::class); + + $result = $instance->execute(1, 'mollie_methods_klarnapaynow'); + + $this->assertFalse($result); + } +} diff --git a/composer.json b/composer.json index 888279918bc..20420f2f33a 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "mollie/magento2", "description": "Mollie Payment Module for Magento 2", - "version": "2.27.0", + "version": "2.28.0", "keywords": [ "mollie", "payment", diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index f5f086bd0a4..a632aa67edb 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -182,28 +182,42 @@ payment/mollie_general/invoice_moment Klarna or Billie payments?
On Authorize: Create a full invoice when the order is authorized.
On Shipment: Create a (partial) invoice when a shipment is created.]]>
- + + Magento\Config\Model\Config\Source\Yesno + payment/mollie_general/enable_manual_capture + Note: This only works with creditcards when the Payments API is enabled and is currently only available for specified merchants.]]> + + Magento\Config\Model\Config\Source\Yesno payment/mollie_general/invoice_notify - + Note: Klarna has an extra option for this.]]> + + + + Magento\Config\Model\Config\Source\Yesno + payment/mollie_general/invoice_notify_klarna + - Magento\Config\Model\Config\Source\Yesno payment/mollie_general/cancel_failed_orders - Mollie\Payment\Model\Adminhtml\Source\RedirectUserWhenTransactionFails payment/mollie_general/redirect_when_transaction_fails_to - Magento\Config\Model\Config\Source\Yesno @@ -213,14 +227,14 @@ Yes: The surcharge is calculated on the total of subtotal + shipping. ]]> - Magento\Config\Model\Config\Source\Yesno payment/mollie_general/currency Force use of base currency for the payment request. Is set to no the selected currency of the storeview will be used for request. - Mollie\Payment\Model\Adminhtml\Source\Locale diff --git a/etc/config.xml b/etc/config.xml index 39c93749202..71ced0d2456 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -3,7 +3,7 @@ - v2.27.0 + v2.28.0 0 0 test @@ -12,6 +12,7 @@ processing authorize 1 + 1 0 1 @@ -31,6 +32,7 @@ 0 + 0 Mollie\Payment\Model\Methods\General diff --git a/etc/di.xml b/etc/di.xml index 573987a7e20..d5e3e1055df 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -106,6 +106,7 @@ Mollie\Payment\Service\Order\TransactionPart\UseSavedPaymentMethod Mollie\Payment\Service\Order\TransactionPart\PhoneNumber Mollie\Payment\Service\Order\TransactionPart\DateOfBirth + Mollie\Payment\Service\Order\TransactionPart\CaptureMode diff --git a/etc/events.xml b/etc/events.xml index 8d0cdb035cf..e55328d4a69 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -60,4 +60,7 @@ + + + diff --git a/view/adminhtml/layout/adminhtml_order_shipment_new.xml b/view/adminhtml/layout/adminhtml_order_shipment_new.xml new file mode 100644 index 00000000000..8121473ace0 --- /dev/null +++ b/view/adminhtml/layout/adminhtml_order_shipment_new.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/view/adminhtml/templates/order/shipment/create/payment_hold_warning.phtml b/view/adminhtml/templates/order/shipment/create/payment_hold_warning.phtml new file mode 100644 index 00000000000..273c7804f62 --- /dev/null +++ b/view/adminhtml/templates/order/shipment/create/payment_hold_warning.phtml @@ -0,0 +1,31 @@ + + +
+ Please note: You are creating a partial shipment, but it's only possible to capture the payment once. + Please double-check you are shipping the correct items. +
+ + diff --git a/view/adminhtml/web/css/styles.less b/view/adminhtml/web/css/styles.less index 6377700050e..49a20a54ebd 100644 --- a/view/adminhtml/web/css/styles.less +++ b/view/adminhtml/web/css/styles.less @@ -223,3 +223,7 @@ a.mollie-tooltip:hover span { background-size: auto 50%; text-indent: 35px; } + +.mollie-manual-capture-warning.message { + margin-top: 20px; +}