diff --git a/Config.php b/Config.php
index 843e3300fd3..cab021bd7bd 100644
--- a/Config.php
+++ b/Config.php
@@ -11,12 +11,14 @@
class Config
{
+ const GENERAL_INVOICE_NOTIFY = 'payment/mollie_general/invoice_notify';
const XML_PATH_STATUS_PENDING_BANKTRANSFER = 'payment/mollie_methods_banktransfer/order_status_pending';
const XML_PATH_STATUS_NEW_PAYMENT_LINK = 'payment/mollie_methods_paymentlink/order_status_new';
const PAYMENT_KLARNAPAYLATER_PAYMENT_SURCHARGE = 'payment/mollie_methods_klarnapaylater/payment_surcharge';
const PAYMENT_KLARNAPAYLATER_PAYMENT_SURCHARGE_TAX_CLASS = 'payment/mollie_methods_klarnapaylater/payment_surcharge_tax_class';
const PAYMENT_KLARNASLICEIT_PAYMENT_SURCHARGE = 'payment/mollie_methods_klarnasliceit/payment_surcharge';
const PAYMENT_KLARNASLICEIT_PAYMENT_SURCHARGE_TAX_CLASS = 'payment/mollie_methods_klarnasliceit/payment_surcharge_tax_class';
+ const PAYMENT_PAYMENTLINK_ALLOW_MARK_AS_PAID = 'payment/mollie_methods_paymentlink/allow_mark_as_paid';
/**
* @var ScopeConfigInterface
@@ -39,6 +41,25 @@ private function getPath($path, $storeId)
return $this->config->getValue($path, ScopeInterface::SCOPE_STORE, $storeId);
}
+ /**
+ * @param $path
+ * @param $storeId
+ * @return bool
+ */
+ private function getFlag($path, $storeId)
+ {
+ return $this->config->isSetFlag($path, ScopeInterface::SCOPE_STORE, $storeId);
+ }
+
+ /**
+ * @param null $storeId
+ * @return bool
+ */
+ public function sendInvoiceEmail($storeId = null)
+ {
+ return $this->getFlag(static::GENERAL_INVOICE_NOTIFY, $storeId);
+ }
+
public function statusPendingBanktransfer($storeId = null)
{
return $this->config->getValue(
@@ -92,4 +113,13 @@ public function klarnaSliceitPaymentSurchargeTaxClass($storeId = null)
{
return $this->getPath(static::PAYMENT_KLARNASLICEIT_PAYMENT_SURCHARGE_TAX_CLASS, $storeId);
}
+
+ /**
+ * @param null $storeId
+ * @return bool
+ */
+ public function paymentlinkAllowMarkAsPaid($storeId = null)
+ {
+ return $this->getFlag(static::PAYMENT_PAYMENTLINK_ALLOW_MARK_AS_PAID, $storeId);
+ }
}
diff --git a/Controller/Adminhtml/Action/MarkAsPaid.php b/Controller/Adminhtml/Action/MarkAsPaid.php
new file mode 100644
index 00000000000..5a57316fd3e
--- /dev/null
+++ b/Controller/Adminhtml/Action/MarkAsPaid.php
@@ -0,0 +1,201 @@
+config = $config;
+ $this->orderRepository = $orderRepository;
+ $this->orderCreate = $orderCreate;
+ $this->quoteSession = $quoteSession;
+ $this->invoiceService = $invoiceService;
+ $this->invoiceSender = $invoiceSender;
+ $this->transactionFactory = $transactionFactory;
+ $this->orderCommentHistory = $orderCommentHistory;
+ }
+
+ /**
+ * This controller recreates the selected order with the checkmo payment method and marks it as completed. The
+ * original order is then canceled.
+ *
+ * {@inheritDoc}
+ */
+ public function execute()
+ {
+ $orderId = $this->getRequest()->getParam('order_id');
+
+ $originalOrder = $this->orderRepository->get($orderId);
+ $originalOrder->setReordered(true);
+
+ $resultRedirect = $this->resultRedirectFactory->create();
+ try {
+ $this->transaction = $this->transactionFactory->create();
+
+ $order = $this->recreateOrder($originalOrder);
+ $invoice = $this->createInvoiceFor($order);
+ $this->cancelOriginalOrder($originalOrder);
+ $this->transaction->save();
+
+ $this->addCommentHistoryOriginalOrder($originalOrder, $order->getIncrementId());
+ $this->sendInvoice($invoice, $order);
+
+ $this->messageManager->addSuccessMessage(
+ __(
+ 'We cancelled order %1, created this order and marked it as complete.',
+ $originalOrder->getIncrementId()
+ )
+ );
+
+ return $resultRedirect->setPath('sales/order/view', ['order_id' => $order->getEntityId()]);
+ } catch (\Exception $exception) {
+ $this->messageManager->addExceptionMessage($exception);
+
+ return $resultRedirect->setPath('sales/order/view', ['order_id' => $originalOrder->getEntityId()]);
+ }
+ }
+
+ /**
+ * @param OrderInterface $originalOrder
+ * @return OrderInterface
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function recreateOrder(OrderInterface $originalOrder)
+ {
+ $this->quoteSession->setOrderId($originalOrder->getEntityId());
+ $this->quoteSession->setUseOldShippingMethod(true);
+ $this->orderCreate->initFromOrder($originalOrder);
+ $this->orderCreate->setPaymentMethod('mollie_methods_reorder');
+
+ $order = $this->orderCreate->createOrder();
+ $order->setState(Order::STATE_PROCESSING);
+ $order->setStatus(Order::STATE_PROCESSING);
+
+ $this->transaction->addObject($order);
+ $this->transaction->addObject($originalOrder);
+
+ return $order;
+ }
+
+ /**
+ * @param OrderInterface $originalOrder
+ */
+ private function cancelOriginalOrder(OrderInterface $originalOrder)
+ {
+ $originalOrder->cancel();
+ }
+
+ /**
+ * @param OrderInterface $originalOrder
+ * @param string $newIncrementId
+ */
+ private function addCommentHistoryOriginalOrder(OrderInterface $originalOrder, $newIncrementId)
+ {
+ $comment = __('We created a new order with increment ID: %1', $newIncrementId);
+ $this->orderCommentHistory->add($originalOrder, $comment, false);
+ }
+
+ private function createInvoiceFor(OrderInterface $order)
+ {
+ $invoice = $this->invoiceService->prepareInvoice($order);
+ $invoice->setRequestedCaptureCase(Invoice::CAPTURE_OFFLINE);
+ $invoice->register();
+
+ $this->transaction->addObject($invoice);
+
+ return $invoice;
+ }
+
+ private function sendInvoice(InvoiceInterface $invoice, OrderInterface $order)
+ {
+ /** @var Order\Invoice $invoice */
+ if ($invoice->getEmailSent() || !$this->config->sendInvoiceEmail($invoice->getStoreId())) {
+ return;
+ }
+
+ try {
+ $this->invoiceSender->send($invoice);
+ $message = __('Notified customer about invoice #%1', $invoice->getIncrementId());
+ $this->orderCommentHistory->add($order, $message, true);
+ } catch (\Throwable $exception) {
+ $message = __('Unable to send the invoice: %1', $exception->getMessage());
+ $this->orderCommentHistory->add($order, $message, false);
+ }
+ }
+}
diff --git a/Model/Methods/Reorder.php b/Model/Methods/Reorder.php
new file mode 100644
index 00000000000..c2fb4cad392
--- /dev/null
+++ b/Model/Methods/Reorder.php
@@ -0,0 +1,112 @@
+request = $request;
+
+ parent::__construct(
+ $context,
+ $registry,
+ $extensionFactory,
+ $customAttributeFactory,
+ $paymentData,
+ $scopeConfig,
+ $logger,
+ $resource,
+ $resourceCollection,
+ $data
+ );
+ }
+
+ /**
+ * @param string $paymentAction
+ * @param object $stateObject
+ *
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Mollie\Api\Exceptions\ApiException
+ */
+ public function initialize($paymentAction, $stateObject)
+ {
+ /** @var \Magento\Sales\Model\Order\Payment $payment */
+ $payment = $this->getInfoInstance();
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $payment->getOrder();
+ $order->setCanSendNewEmailFlag(false);
+ $order->save();
+ }
+
+ public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null)
+ {
+ return $this->request->getModuleName() == 'mollie' && $this->request->getActionName() == 'markAsPaid';
+ }
+}
diff --git a/Model/OrderLines.php b/Model/OrderLines.php
index 75fd942164f..e701c875aec 100644
--- a/Model/OrderLines.php
+++ b/Model/OrderLines.php
@@ -12,6 +12,7 @@
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Model\AbstractModel;
+use Magento\Sales\Api\Data\CreditmemoInterface;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Item;
use Mollie\Payment\Model\ResourceModel\OrderLines\Collection as OrderLinesCollection;
@@ -416,12 +417,11 @@ public function getOrderLineByItemId($itemId)
}
/**
- * @param Order\Creditmemo $creditmemo
- * @param $addShipping
- *
+ * @param CreditmemoInterface $creditmemo
+ * @param bool $addShipping
* @return array
*/
- public function getCreditmemoOrderLines($creditmemo, $addShipping)
+ public function getCreditmemoOrderLines(CreditmemoInterface $creditmemo, $addShipping)
{
$orderLines = [];
@@ -429,9 +429,23 @@ public function getCreditmemoOrderLines($creditmemo, $addShipping)
foreach ($creditmemo->getAllItems() as $item) {
$orderItemId = $item->getOrderItemId();
$lineId = $this->getOrderLineByItemId($orderItemId)->getLineId();
- if ($lineId) {
- $orderLines[] = ['id' => $lineId, 'quantity' => round($item->getQty())];
+ if (!$lineId) {
+ continue;
}
+
+ $line = [
+ 'id' => $lineId,
+ 'quantity' => round($item->getQty()),
+ ];
+
+ if ($item->getBaseDiscountAmount()) {
+ $line['amount'] = $this->mollieHelper->getAmountArray(
+ $creditmemo->getBaseCurrencyCode(),
+ $item->getBaseRowTotalInclTax() - $item->getBaseDiscountAmount()
+ );
+ }
+
+ $orderLines[] = $line;
}
$orderId = $creditmemo->getOrderId();
diff --git a/Observer/OrderCancelAfter.php b/Observer/OrderCancelAfter.php
index 157bffddbbd..dbb11b7559c 100644
--- a/Observer/OrderCancelAfter.php
+++ b/Observer/OrderCancelAfter.php
@@ -52,6 +52,13 @@ public function execute(Observer $observer)
/** @var \Magento\Sales\Model\Order $order */
$order = $observer->getEvent()->getorder();
+ /**
+ * When manually marking an order as paid we don't want to communicate to Mollie as it will throw an exception.
+ */
+ if ($order->getReordered()) {
+ return;
+ }
+
if ($this->mollieHelper->isPaidUsingMollieOrdersApi($order)) {
$this->mollieModel->cancelOrder($order);
}
diff --git a/Plugin/Sales/Block/Adminhtml/Order/View.php b/Plugin/Sales/Block/Adminhtml/Order/View.php
new file mode 100644
index 00000000000..5eb64883ae9
--- /dev/null
+++ b/Plugin/Sales/Block/Adminhtml/Order/View.php
@@ -0,0 +1,82 @@
+config = $config;
+ $this->url = $url;
+ $this->orderRepository = $orderRepository;
+ $this->paymentHelper = $paymentHelper;
+ }
+
+ public function beforeSetLayout(Subject $subject)
+ {
+ $order = $subject->getOrder();
+ if (!$this->config->paymentlinkAllowMarkAsPaid($order->getStoreId())) {
+ return;
+ }
+
+ $instance = $this->paymentHelper->getMethodInstance(Checkmo::PAYMENT_METHOD_CHECKMO_CODE);
+
+ $isAvailable = !$instance->isAvailable();
+ if (!$order->canCancel() ||
+ $order->getPayment()->getMethod() != 'mollie_methods_paymentlink' ||
+ !$instance ||
+ $isAvailable
+ ) {
+ return;
+ }
+
+ $message = __('Are you sure you want to do this? ' .
+ 'This will cancel the current order and create a new one that is marked as payed.');
+ $url = $this->url->getUrl('mollie/action/markAsPaid/');
+
+ $subject->addButton(
+ 'mollie_payment_mark_as_payed',
+ [
+ 'label' => __('Mark as paid'),
+ 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $url . '\', {data: {order_id: ' . $subject->getOrderId() . '}})',
+ ],
+ 0,
+ -10
+ );
+ }
+}
diff --git a/Test/Integration/Model/OrderLinesTest.php b/Test/Integration/Model/OrderLinesTest.php
index dc2f55a4716..1f7d93e4859 100644
--- a/Test/Integration/Model/OrderLinesTest.php
+++ b/Test/Integration/Model/OrderLinesTest.php
@@ -3,6 +3,7 @@
namespace Mollie\Payment\Model;
use Magento\Sales\Api\Data\CreditmemoInterface;
+use Magento\Sales\Api\Data\CreditmemoItemInterface;
use Mollie\Payment\Test\Integration\IntegrationTestCase;
class OrderLinesTest extends IntegrationTestCase
@@ -91,6 +92,37 @@ public function testGetCreditmemoOrderLinesIncludesTheStoreCredit()
$this->assertEquals(1, $line['quantity']);
}
+ public function testCreditmemoUsesTheDiscount()
+ {
+ /** @var OrderLines $orderLine */
+ $orderLine = $this->objectManager->get(\Mollie\Payment\Model\OrderLinesFactory::class)->create();
+ $orderLine->setItemId(999);
+ $orderLine->setLineId('ord_abc123');
+ $orderLine->save();
+
+ /** @var CreditmemoItemInterface $creditmemoItem */
+ $creditmemoItem = $this->objectManager->create(CreditmemoItemInterface::class);
+ $creditmemoItem->setBaseRowTotalInclTax(45);
+ $creditmemoItem->setBaseDiscountAmount(9);
+ $creditmemoItem->setQty(1);
+ $creditmemoItem->setOrderItemId(999);
+
+ /** @var CreditmemoInterface $creditmemo */
+ $creditmemo = $this->objectManager->get(CreditmemoInterface::class);
+ $creditmemo->setOrderId(999);
+ $creditmemo->setItems([$creditmemoItem]);
+
+ /** @var OrderLines $instance */
+ $instance = $this->objectManager->get(OrderLines::class);
+ $result = $instance->getCreditmemoOrderLines($creditmemo, false);
+
+ $this->assertCount(1, $result['lines']);
+
+ $line = $result['lines'][0];
+ $this->assertEquals('36', $line['amount']['value']);
+ $this->assertEquals(1, $line['quantity']);
+ }
+
private function rollbackCreditmemos()
{
$collection = $this->objectManager->get(\Mollie\Payment\Model\ResourceModel\OrderLines\Collection::class);
diff --git a/Test/Integration/Plugin/Sales/Block/Adminhtml/Order/ViewTest.php b/Test/Integration/Plugin/Sales/Block/Adminhtml/Order/ViewTest.php
new file mode 100644
index 00000000000..ddd97027e3d
--- /dev/null
+++ b/Test/Integration/Plugin/Sales/Block/Adminhtml/Order/ViewTest.php
@@ -0,0 +1,106 @@
+createMock(Order::class);
+
+ $subjectMock = $this->createMock(Subject::class);
+ $subjectMock->method('getOrder')->willReturn($orderMock);
+
+ $subjectMock->expects($this->never())->method('addButton');
+
+ /** @var View $instance */
+ $instance = $this->objectManager->create(View::class);
+ $instance->beforeSetLayout($subjectMock);
+ }
+
+ /**
+ * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/allow_mark_as_paid 1
+ */
+ public function testDoesNotShowsTheButtonWhenWeCantCancel()
+ {
+ $orderMock = $this->createMock(Order::class);
+ $orderMock->method('canCancel')->willReturn(false);
+
+ $subjectMock = $this->createMock(Subject::class);
+ $subjectMock->method('getOrder')->willReturn($orderMock);
+
+ $subjectMock->expects($this->never())->method('addButton');
+
+ /** @var View $instance */
+ $instance = $this->objectManager->create(View::class);
+ $instance->beforeSetLayout($subjectMock);
+ }
+
+ /**
+ * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/allow_mark_as_paid 1
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ */
+ public function testDoesNotShowsTheButtonWhenNotPaymentLink()
+ {
+ $order = $this->loadOrderById('100000001');
+ $order->getPayment()->setMethod('checkmo');
+
+ $subjectMock = $this->createMock(Subject::class);
+ $subjectMock->method('getOrder')->willReturn($order);
+
+ $subjectMock->expects($this->never())->method('addButton');
+
+ /** @var View $instance */
+ $instance = $this->objectManager->create(View::class);
+ $instance->beforeSetLayout($subjectMock);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/allow_mark_as_paid 0
+ */
+ public function testDoesNotShowsTheButtonWhenMollieReorderIsNotAvailable()
+ {
+ $order = $this->loadOrderById('100000001');
+ $order->getPayment()->setMethod('mollie_methods_paymentlink');
+
+ $subjectMock = $this->createMock(Subject::class);
+ $subjectMock->method('getOrder')->willReturn($order);
+
+ $subjectMock->expects($this->never())->method('addButton');
+
+ /** @var View $instance */
+ $instance = $this->objectManager->create(View::class);
+ $instance->beforeSetLayout($subjectMock);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ * @magentoConfigFixture default_store payment/mollie_methods_paymentlink/allow_mark_as_paid 1
+ */
+ public function testShowTheButtonWhenApplicable()
+ {
+ $order = $this->loadOrderById('100000001');
+ $order->getPayment()->setMethod('mollie_methods_paymentlink');
+
+ $subjectMock = $this->createMock(Subject::class);
+ $subjectMock->method('getOrder')->willReturn($order);
+
+ $subjectMock->expects($this->once())->method('addButton');
+
+ /** @var View $instance */
+ $instance = $this->objectManager->create(View::class);
+ $instance->beforeSetLayout($subjectMock);
+ }
+}
diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml
new file mode 100644
index 00000000000..f638006dbb0
--- /dev/null
+++ b/etc/adminhtml/di.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/etc/adminhtml/methods/paymentlink.xml b/etc/adminhtml/methods/paymentlink.xml
index e00dcc111e1..37ac8427e5f 100644
--- a/etc/adminhtml/methods/paymentlink.xml
+++ b/etc/adminhtml/methods/paymentlink.xml
@@ -27,7 +27,16 @@
1
-
+
+ Magento\Config\Model\Config\Source\Yesno
+ payment/mollie_methods_paymentlink/allow_mark_as_paid
+
+ 1
+
+
+
Magento\Config\Model\Config\Source\Yesno
@@ -38,7 +47,7 @@
1
-
+
Mollie\Payment\Model\Adminhtml\Source\NewStatus
payment/mollie_methods_paymentlink/order_status_new
@@ -46,7 +55,7 @@
1
-
Magento\Payment\Model\Config\Source\Allspecificcountries
@@ -55,7 +64,7 @@
1
-
Magento\Directory\Model\Config\Source\Country
@@ -65,7 +74,7 @@
1
-
payment/mollie_methods_paymentlink/min_order_total
@@ -73,7 +82,7 @@
1
-
payment/mollie_methods_paymentlink/max_order_total
@@ -81,7 +90,7 @@
1
-
validate-number
@@ -90,7 +99,7 @@
1
-
validate-digits-range digits-range-1-100
diff --git a/etc/config.xml b/etc/config.xml
index 54afbf0e8f7..20956f31719 100644
--- a/etc/config.xml
+++ b/etc/config.xml
@@ -172,6 +172,15 @@
here to pay for your purchase.]]>
+
+ 1
+ Mollie\Payment\Model\Methods\Reorder
+ Mollie: Reorder
+ {ordernumber}
+ order
+ order
+ 0
+
1
Mollie\Payment\Model\Methods\Giftcard
diff --git a/view/frontend/web/js/view/checkout/save-payment-method.js b/view/frontend/web/js/view/checkout/save-payment-method.js
index 2a495f3a58f..ef407c5c5a0 100644
--- a/view/frontend/web/js/view/checkout/save-payment-method.js
+++ b/view/frontend/web/js/view/checkout/save-payment-method.js
@@ -1,4 +1,5 @@
define([
+ 'ko',
'uiComponent',
'mage/storage',
'Magento_Checkout/js/model/quote',
@@ -6,6 +7,7 @@ define([
'Magento_Checkout/js/model/totals',
'Magento_Checkout/js/action/get-totals'
], function (
+ ko,
Component,
storage,
quote,
@@ -54,11 +56,26 @@ define([
var url = resourceUrlManager.getUrl(urls, params);
payload.paymentMethod = {
- method: method
+ method: method,
+ extension_attributes: {}
};
payload.billingAddress = quote.billingAddress();
+ /**
+ * Problem: We need to set the payment method, therefor we created this function. The api call requires
+ * that the agreements are all agreed by before doing any action. That's why we list all agreement ids
+ * and sent them with the request. In a later point in the checkout this will also be checked.
+ */
+ var config = window.checkoutConfig.checkoutAgreements;
+ if (config.isEnabled) {
+ var ids = config.agreements.map( function (agreement) {
+ return agreement.agreementId;
+ });
+
+ payload.paymentMethod.extension_attributes.agreement_ids = ids;
+ }
+
totals.isLoading(true);
storage.post(
url,