diff --git a/composer.json b/composer.json
index 4100eb24..415e9296 100644
--- a/composer.json
+++ b/composer.json
@@ -48,7 +48,10 @@
"Mollie\\WooCommerceTests\\": "tests/php",
"Mollie\\WooCommerceTests\\Unit\\": "tests/php/Unit",
"Mollie\\WooCommerceTests\\Functional\\": "tests/php/Functional"
- }
+ },
+ "files": [
+ "inc/api/order-functions.php"
+ ]
},
"scripts": {
"check-coding-standards": "vendor/bin/phpcs --parallel=8 -s",
diff --git a/documentation/Plugin-API-Functions.md b/documentation/Plugin-API-Functions.md
new file mode 100644
index 00000000..be96422b
--- /dev/null
+++ b/documentation/Plugin-API-Functions.md
@@ -0,0 +1,65 @@
+### Programmatically capture, refund, void, cancel, and ship Mollie orders.
+
+With the Mollie API, you can programmatically capture, refund, void, cancel, and ship orders.
+These actions are logged by the plugin.
+Here are some examples of how to use these functions:
+
+#### Capture an order
+```php
+use function Mollie\WooCommerce\Inc\Api\mollie_capture_order;
+
+add_action('init', function () {
+ $order_id = 123;
+ $order = wc_get_order($order_id);
+ mollie_capture_order($order);
+});
+```
+
+#### Refund an order
+```php
+use function Mollie\WooCommerce\Inc\Api\mollie_refund_order;
+
+add_action('init', function () {
+ $order_id = 123;
+ $order = wc_get_order($order_id);
+ $refund = mollie_refund_order($order, 10.00, 'Refund reason');
+ // $refund is an instance of Mollie\Api\Resources\Refund
+});
+```
+
+#### Void an order
+```php
+use function Mollie\WooCommerce\Inc\Api\mollie_void_order;
+
+add_action('init', function () {
+ $order_id = 123;
+ $order = wc_get_order($order_id);
+ mollie_void_order($order);
+});
+```
+
+#### Cancel an order
+```php
+use function Mollie\WooCommerce\Inc\Api\mollie_cancel_order;
+
+add_action('init', function () {
+ $order_id = 123;
+ $order = wc_get_order($order_id);
+ mollie_cancel_order($order);
+});
+```
+
+#### Ship an order
+```php
+use function Mollie\WooCommerce\Inc\Api\mollie_ship_order;
+
+add_action('init', function () {
+ $order_id = 123;
+ $order = wc_get_order($order_id);
+ mollie_ship_order($order);
+});
+```
+
+
+
+
diff --git a/inc/api/order-functions.php b/inc/api/order-functions.php
new file mode 100644
index 00000000..5af9c899
--- /dev/null
+++ b/inc/api/order-functions.php
@@ -0,0 +1,88 @@
+captureOrder($wc_order);
+}
+
+/**
+ * Refunds the Mollie order.
+ *
+ * @param WC_Order $wc_order The WC order.
+ * @param float $amount The refund amount.
+ * @param string $reason The reason for the refund.
+ * @return \WP_Error|Refund The result of the refund operation.
+ */
+function mollie_refund_order(WC_Order $wc_order, float $amount, string $reason = '')
+{
+
+ $mollieApi = MolliePluginApi::getInstance();
+ return $mollieApi->refundOrder($wc_order, $amount, $reason);
+}
+
+/**
+ * Voids the authorization.
+ * Logs the result of the operation.
+ *
+ * @param WC_Order $wc_order The WC order.
+ *
+ */
+function mollie_void_order(WC_Order $wc_order): void
+{
+
+ $mollieApi = MolliePluginApi::getInstance();
+ $mollieApi->voidOrder($wc_order);
+}
+
+/**
+ * Cancels the order at Mollie and also in WooCommerce if was not already done.
+ * Logs the result of the operation.
+ *
+ * @param WC_Order $wc_order The WC order.
+ */
+function mollie_cancel_order(WC_Order $wc_order): void
+{
+
+ $order_id = $wc_order->get_id();
+ $mollieApi = MolliePluginApi::getInstance();
+ $mollieApi->cancelOrder((string)$order_id);
+}
+
+/**
+ * Ship all order lines and capture an order at Mollie.
+ * Logs the result of the operation.
+ *
+ * @param WC_Order $wc_order The WC order.
+ *
+ */
+function mollie_ship_order(WC_Order $wc_order): void
+{
+ $order_id = $wc_order->get_id();
+ $mollieApi = MolliePluginApi::getInstance();
+ $mollieApi->shipOrderAndCapture((string)$order_id);
+}
diff --git a/mollie-payments-for-woocommerce.php b/mollie-payments-for-woocommerce.php
index 2085bf53..cfd55f11 100644
--- a/mollie-payments-for-woocommerce.php
+++ b/mollie-payments-for-woocommerce.php
@@ -26,6 +26,7 @@
use Mollie\WooCommerce\Activation\ActivationModule;
use Mollie\WooCommerce\Activation\ConstraintsChecker;
use Mollie\WooCommerce\Assets\AssetsModule;
+use Mollie\WooCommerce\PluginApi\PluginApiModule;
use Mollie\WooCommerce\Shared\SharedModule;
use Mollie\WooCommerce\Gateway\GatewayModule;
use Mollie\WooCommerce\Gateway\Voucher\VoucherModule;
@@ -165,9 +166,14 @@ function initialize()
new PaymentModule(),
new MerchantCaptureModule(),
new UninstallModule(),
+ new PluginApiModule(),
];
$modules = apply_filters('mollie_wc_plugin_modules', $modules);
- $bootstrap->boot(...$modules);
+ foreach ($modules as $module) {
+ $bootstrap->addModule($module);
+ }
+ $bootstrap->boot();
+
} catch (Throwable $throwable) {
handleException($throwable);
}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 3f6cd519..33e29a68 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -7,7 +7,7 @@
./src
./tests/
-
+
diff --git a/src/Gateway/GatewayModule.php b/src/Gateway/GatewayModule.php
index 72cade0d..b8987f10 100644
--- a/src/Gateway/GatewayModule.php
+++ b/src/Gateway/GatewayModule.php
@@ -707,6 +707,7 @@ public function buildPaymentMethod(
Surcharge $surchargeService,
array $apiMethod
) {
+
$transformedId = ucfirst($id);
$paymentMethodClassName = 'Mollie\\WooCommerce\\PaymentMethods\\' . $transformedId;
$paymentMethod = new $paymentMethodClassName(
diff --git a/src/Gateway/MolliePaymentGateway.php b/src/Gateway/MolliePaymentGateway.php
index e880f483..10750ef9 100644
--- a/src/Gateway/MolliePaymentGateway.php
+++ b/src/Gateway/MolliePaymentGateway.php
@@ -27,6 +27,7 @@
class MolliePaymentGateway extends WC_Payment_Gateway implements MolliePaymentGatewayI
{
+ const ORDER_ID_META_KEY = '_mollie_order_id';
/**
* @var bool
*/
@@ -708,70 +709,7 @@ protected function activePaymentObject($orderId, $useCache): Payment
*/
public function process_refund($order_id, $amount = null, $reason = '')
{
- // Get the WooCommerce order
- $order = wc_get_order($order_id);
-
- // WooCommerce order not found
- if (!$order) {
- $error_message = "Could not find WooCommerce order $order_id.";
-
- $this->logger->debug(
- __METHOD__ . ' - ' . $error_message
- );
-
- return new WP_Error('1', $error_message);
- }
-
- // Check if there is a Mollie Payment Order object connected to this WooCommerce order
- $payment_object_id = $this->paymentObject()->getActiveMollieOrderId(
- $order_id
- );
-
- // If there is no Mollie Payment Order object, try getting a Mollie Payment Payment object
- if (!$payment_object_id) {
- $payment_object_id = $this->paymentObject()
- ->getActiveMolliePaymentId($order_id);
- }
-
- // Mollie Payment object not found
- if (!$payment_object_id) {
- $error_message = "Can\'t process refund. Could not find Mollie Payment object id for order $order_id.";
-
- $this->logger->debug(
- __METHOD__ . ' - ' . $error_message
- );
-
- return new WP_Error('1', $error_message);
- }
-
- try {
- $payment_object = $this->paymentFactory
- ->getPaymentObject(
- $payment_object_id
- );
- } catch (ApiException $exception) {
- $exceptionMessage = $exception->getMessage();
- $this->logger->debug($exceptionMessage);
- return new WP_Error('error', $exceptionMessage);
- }
-
- if (!$payment_object) {
- $error_message = "Can\'t process refund. Could not find Mollie Payment object data for order $order_id.";
-
- $this->logger->debug(
- __METHOD__ . ' - ' . $error_message
- );
-
- return new WP_Error('1', $error_message);
- }
-
- return $payment_object->refund(
- $order,
- $order_id,
- $payment_object,
- $amount,
- $reason
- );
+ return $this->paymentObject()->processRefund($order_id, $amount, $reason);
}
/**
@@ -1029,8 +967,8 @@ public function getSelectedIssuer(): ?string
*/
public function get_transaction_url($order): string
{
- $isPaymentApi = substr($order->get_meta('_mollie_order_id', true), 0, 3) === 'tr_' ;
- $resource = ($order->get_meta('_mollie_order_id', true) && !$isPaymentApi) ? 'orders' : 'payments';
+ $isPaymentApi = substr($order->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY, true), 0, 3) === 'tr_' ;
+ $resource = ($order->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY, true) && !$isPaymentApi) ? 'orders' : 'payments';
$this->view_transaction_url = 'https://my.mollie.com/dashboard/'
. $resource . '/%s?utm_source=woocommerce&utm_medium=plugin&utm_campaign=partner';
diff --git a/src/Payment/MollieObject.php b/src/Payment/MollieObject.php
index d2f44480..25cc08e4 100644
--- a/src/Payment/MollieObject.php
+++ b/src/Payment/MollieObject.php
@@ -15,6 +15,7 @@
use WC_Payment_Gateway;
use Psr\Log\LoggerInterface as Logger;
use stdClass;
+use WP_Error;
class MollieObject
{
@@ -72,6 +73,79 @@ public function customerId()
return self::$customerId;
}
+ /**
+ * @param int $order_id
+ * @param float|null $amount
+ * @param string $reason
+ * @return bool|WP_Error
+ */
+ public function processRefund(int $order_id, ?float $amount, string $reason)
+ {
+ // Get the WooCommerce order
+ $order = wc_get_order($order_id);
+
+ // WooCommerce order not found
+ if (!$order) {
+ $error_message = "Could not find WooCommerce order $order_id.";
+
+ $this->logger->debug(
+ __METHOD__ . ' - ' . $error_message
+ );
+
+ return new WP_Error('1', $error_message);
+ }
+
+ // Check if there is a Mollie Payment Order object connected to this WooCommerce order
+ $payment_object_id = $this->getActiveMollieOrderId(
+ $order_id
+ );
+
+ // If there is no Mollie Payment Order object, try getting a Mollie Payment Payment object
+ if (!$payment_object_id) {
+ $payment_object_id = $this->getActiveMolliePaymentId($order_id);
+ }
+
+ // Mollie Payment object not found
+ if (!$payment_object_id) {
+ $error_message = "Can\'t process refund. Could not find Mollie Payment object id for order $order_id.";
+
+ $this->logger->debug(
+ __METHOD__ . ' - ' . $error_message
+ );
+
+ return new WP_Error('1', $error_message);
+ }
+
+ try {
+ $payment_object = $this->paymentFactory
+ ->getPaymentObject(
+ $payment_object_id
+ );
+ } catch (ApiException $exception) {
+ $exceptionMessage = $exception->getMessage();
+ $this->logger->debug($exceptionMessage);
+ return new WP_Error('error', $exceptionMessage);
+ }
+
+ if (!$payment_object) {
+ $error_message = "Can\'t process refund. Could not find Mollie Payment object data for order $order_id.";
+
+ $this->logger->debug(
+ __METHOD__ . ' - ' . $error_message
+ );
+
+ return new WP_Error('1', $error_message);
+ }
+
+ return $payment_object->refund(
+ $order,
+ $order_id,
+ $payment_object,
+ $amount,
+ $reason
+ );
+ }
+
/**
* Get Mollie payment from cache or load from Mollie
* Skip cache by setting $use_cache to false
@@ -171,7 +245,7 @@ public function setActiveMolliePaymentForOrders($order_id)
{
static::$order = wc_get_order($order_id);
- static::$order->update_meta_data('_mollie_order_id', $this->data->id);
+ static::$order->update_meta_data(MolliePaymentGateway::ORDER_ID_META_KEY, $this->data->id);
static::$order->update_meta_data('_mollie_payment_id', static::$paymentId);
static::$order->update_meta_data('_mollie_payment_mode', $this->data->mode);
@@ -325,7 +399,7 @@ public function getActiveMolliePaymentId($order_id)
public function getActiveMollieOrderId($order_id)
{
$order = wc_get_order($order_id);
- return $order->get_meta('_mollie_order_id', true);
+ return $order->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY, true);
}
/**
@@ -409,6 +483,85 @@ public function hasActiveMollieOrder($order_id)
return ! empty($mollie_payment_id);
}
+ /**
+ * Cancel an order at Mollie.
+ *
+ */
+ public function cancelOrderAtMollie($orderId)
+ {
+ $order = wc_get_order($orderId);
+
+ if (!$order) {
+ // Order not found, log and exit early.
+ $this->logger->debug(__METHOD__ . ' - Order not found.');
+ return;
+ }
+
+ // Does WooCommerce order contain a Mollie payment?
+ if (strstr($order->get_payment_method(), 'mollie_wc_gateway_') === false) {
+ return;
+ }
+
+ // To disable automatic canceling of the Mollie order when a WooCommerce order status is updated to canceled,
+ // store an option 'mollie-payments-for-woocommerce_disableCancelOrderAtMollie' with value 1
+ if (get_option($this->pluginId . '_' . 'disableCancelOrderAtMollie', '0') === '1') {
+ return;
+ }
+
+ $this->logger->debug(__METHOD__ . ' - ' . $orderId . ' - Try to process cancelled order at Mollie.');
+
+ $mollie_order_id = ( $mollie_order_id = $order->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY, true) ) ? $mollie_order_id : false;
+
+ if ($mollie_order_id === false) {
+ $message = _x('Order contains Mollie payment method, but not a valid Mollie Order ID. Canceling order failed.', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $orderId . ' - Order contains Mollie payment method, but not a valid Mollie Order ID. Canceling order failed.');
+
+ return;
+ }
+
+ $orderStr = "ord_";
+ if (substr($mollie_order_id, 0, strlen($orderStr)) !== $orderStr) {
+ $this->logger->debug(__METHOD__ . ' - ' . $orderId . ' - Order uses Payment API, cannot cancel as order.');
+
+ return;
+ }
+
+ $apiKey = $this->settingsHelper->getApiKey();
+ try {
+ // Get the order from the Mollie API
+ $mollie_order = $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id);
+
+ // Check that order is not already canceled at Mollie
+ if ($mollie_order->isCanceled()) {
+ $message = _x('Order already canceled at Mollie, can not be canceled again.', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $orderId . ' - Order already canceled at Mollie, can not be canceled again.');
+
+ return;
+ }
+
+ // Check that order has the correct status to be canceled
+ if ($mollie_order->isCreated() || $mollie_order->isAuthorized() || $mollie_order->isShipping()) {
+ $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id)->cancel();
+ $message = _x('Order also cancelled at Mollie.', 'Order note info', 'mollie-payments-for-woocommerce');
+ //todo Check status of the Woo order and cancel it if it is not already cancelled
+
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $orderId . ' - Order cancelled in WooCommerce, also cancelled at Mollie.');
+
+ return;
+ }
+ $message = _x('Order could not be canceled at Mollie, because order status is ', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message . $mollie_order->status . '.');
+ $this->logger->debug(__METHOD__ . ' - ' . $orderId . ' - Order could not be canceled at Mollie, because order status is ' . $mollie_order->status . '.');
+ } catch (ApiException $e) {
+ $this->logger->debug(__METHOD__ . ' - ' . $orderId . ' - Updating order to canceled at Mollie failed, error: ' . $e->getMessage());
+ }
+
+ return;
+ }
+
/**
* @param int $order_id
* @param string $payment_id
@@ -469,6 +622,84 @@ public function hasCancelledMolliePayment($order_id)
return ! empty($cancelled_payment_id);
}
+ /**
+ * Ship all order lines and capture an order at Mollie.
+ *
+ */
+ public function shipAndCaptureOrderAtMollie($order_id)
+ {
+ $order = wc_get_order($order_id);
+
+ if (!$order) {
+ // Order not found, log and exit early.
+ $this->logger->debug(__METHOD__ . ' - Order not found.');
+ return;
+ }
+
+ // Does WooCommerce order contain a Mollie payment?
+ if (strstr($order->get_payment_method(), 'mollie_wc_gateway_') === false) {
+ return;
+ }
+
+ // To disable automatic shipping and capturing of the Mollie order when a WooCommerce order status is updated to completed,
+ // store an option 'mollie-payments-for-woocommerce_disableShipOrderAtMollie' with value 1
+ if (apply_filters('mollie_wc_gateway_disable_ship_and_capture', get_option($this->pluginId . '_' . 'disableShipOrderAtMollie', '0') === '1', $order)) {
+ return;
+ }
+
+ $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Try to process completed order for a potential capture at Mollie.');
+
+ // Does WooCommerce order contain a Mollie Order?
+ $mollie_order_id = ( $mollie_order_id = $order->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY, true) ) ? $mollie_order_id : false;
+ // Is it a payment? you cannot ship a payment
+ if ($mollie_order_id === false || substr($mollie_order_id, 0, 3) === 'tr_') {
+ $message = _x('Processing a payment, no capture needed', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Processing a payment, no capture needed.');
+
+ return;
+ }
+
+ $apiKey = $this->settingsHelper->getApiKey();
+ try {
+ // Get the order from the Mollie API
+ $mollie_order = $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id);
+
+ // Check that order is Paid or Authorized and can be captured
+ if ($mollie_order->isCanceled()) {
+ $message = _x('Order already canceled at Mollie, can not be shipped/captured.', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order already canceled at Mollie, can not be shipped/captured.');
+
+ return;
+ }
+
+ if ($mollie_order->isCompleted()) {
+ $message = _x('Order already completed at Mollie, can not be shipped/captured.', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order already completed at Mollie, can not be shipped/captured.');
+
+ return;
+ }
+
+ if ($mollie_order->isPaid() || $mollie_order->isAuthorized()) {
+ $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id)->shipAll();
+ $message = _x('Order successfully updated to shipped at Mollie, capture of funds underway.', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order successfully updated to shipped at Mollie, capture of funds underway.');
+
+ return;
+ }
+ $message = _x('Order not paid or authorized at Mollie yet, can not be shipped.', 'Order note info', 'mollie-payments-for-woocommerce');
+ $order->add_order_note($message);
+ $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order not paid or authorized at Mollie yet, can not be shipped.');
+ } catch (ApiException $e) {
+ $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Processing shipment & capture failed, error: ' . $e->getMessage());
+ }
+
+ return;
+ }
+
public function getMolliePaymentIdFromPaymentObject()
{
}
diff --git a/src/Payment/MollieOrder.php b/src/Payment/MollieOrder.php
index 62557c57..13c15873 100644
--- a/src/Payment/MollieOrder.php
+++ b/src/Payment/MollieOrder.php
@@ -172,7 +172,7 @@ public function setActiveMolliePayment($orderId)
self::$customerId = $this->getMollieCustomerIdFromPaymentObject();
self::$order = wc_get_order($orderId);
- self::$order->update_meta_data('_mollie_order_id', $this->data->id);
+ self::$order->update_meta_data(MolliePaymentGateway::ORDER_ID_META_KEY, $this->data->id);
self::$order->save();
return parent::setActiveMolliePayment($orderId);
@@ -511,7 +511,7 @@ public function onWebhookFailed(WC_Order $order, $payment, $paymentMethodTitle)
public function onWebhookExpired(WC_Order $order, $payment, $paymentMethodTitle)
{
$orderId = $order->get_id();
- $molliePaymentId = $order->get_meta('_mollie_order_id', true);
+ $molliePaymentId = $order->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY, true);
// Add messages to log
$this->logger->debug(__METHOD__ . ' called for order ' . $orderId);
@@ -601,7 +601,12 @@ public function refund(WC_Order $order, $orderId, $paymentObject, $amount = null
$refunds = $order->get_refunds();
// Get latest refund
- $woocommerceRefund = wc_get_order($refunds[0]);
+ $woocommerceRefund = isset($refunds[0]) ? wc_get_order($refunds[0]) : false;
+
+ if(empty($woocommerceRefund)) {
+ $this->logger->debug(__METHOD__ . ' - No WooCoommerce refunds found for order ' . $orderId . ' perfoming an amount refund to Mollie API');
+ return $this->refund_amount($order, $amount, $paymentObject, $reason);
+ }
// Get order items from refund
$items = $woocommerceRefund->get_items([ 'line_item', 'fee', 'shipping' ]);
diff --git a/src/Payment/PaymentModule.php b/src/Payment/PaymentModule.php
index 79864077..85cb31fc 100644
--- a/src/Payment/PaymentModule.php
+++ b/src/Payment/PaymentModule.php
@@ -104,12 +104,22 @@ public function run(ContainerInterface $container): bool
// Show Mollie instructions on order details page
add_action('woocommerce_order_details_after_order_table', [ $this, 'onOrderDetails' ], 10, 1);
-
+ // Custom filter to choose on which action to cancel orders at Mollie
+ $cancelOrderAction = apply_filters('mollie-payments-for-woocommerce_cancel_order_action', 'woocommerce_order_status_cancelled');
// Cancel order at Mollie (for Orders API/Klarna)
- add_action('woocommerce_order_status_cancelled', [ $this, 'cancelOrderAtMollie' ]);
-
+ add_action($cancelOrderAction, function ($orderId) use ($container) {
+ $mollieObject = $container->get(MollieObject::class);
+ assert($mollieObject instanceof MollieObject);
+ $mollieObject->cancelOrderAtMollie($orderId);
+ });
+ // Custom filter to choose on which action to ship and capture orders at Mollie
+ $captureOrderAction = apply_filters('mollie-payments-for-woocommerce_ship_capture_order_action', 'woocommerce_order_status_completed');
// Capture order at Mollie (for Orders API/Klarna)
- add_action('woocommerce_order_status_completed', [ $this, 'shipAndCaptureOrderAtMollie' ]);
+ add_action($captureOrderAction, function ($orderId) use ($container) {
+ $mollieObject = $container->get(MollieObject::class);
+ assert($mollieObject instanceof MollieObject);
+ $mollieObject->shipAndCaptureOrderAtMollie($orderId);
+ });
add_filter(
'woocommerce_cancel_unpaid_order',
@@ -331,148 +341,6 @@ public function onOrderDetails(WC_Order $order)
$gateway->displayInstructions($order);
}
- /**
- * Ship all order lines and capture an order at Mollie.
- *
- */
- public function shipAndCaptureOrderAtMollie($order_id)
- {
- $order = wc_get_order($order_id);
-
- // Does WooCommerce order contain a Mollie payment?
- if (strstr($order->get_payment_method(), 'mollie_wc_gateway_') === false) {
- return;
- }
-
- // To disable automatic shipping and capturing of the Mollie order when a WooCommerce order status is updated to completed,
- // store an option 'mollie-payments-for-woocommerce_disableShipOrderAtMollie' with value 1
- if (apply_filters('mollie_wc_gateway_disable_ship_and_capture', get_option($this->pluginId . '_' . 'disableShipOrderAtMollie', '0') === '1', $order)) {
- return;
- }
-
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Try to process completed order for a potential capture at Mollie.');
-
- // Does WooCommerce order contain a Mollie Order?
- $mollie_order_id = ( $mollie_order_id = $order->get_meta('_mollie_order_id', true) ) ? $mollie_order_id : false;
- // Is it a payment? you cannot ship a payment
- if ($mollie_order_id === false || substr($mollie_order_id, 0, 3) === 'tr_') {
- $message = _x('Processing a payment, no capture needed', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Processing a payment, no capture needed.');
-
- return;
- }
-
- $apiKey = $this->settingsHelper->getApiKey();
- try {
- // Get the order from the Mollie API
- $mollie_order = $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id);
-
- // Check that order is Paid or Authorized and can be captured
- if ($mollie_order->isCanceled()) {
- $message = _x('Order already canceled at Mollie, can not be shipped/captured.', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order already canceled at Mollie, can not be shipped/captured.');
-
- return;
- }
-
- if ($mollie_order->isCompleted()) {
- $message = _x('Order already completed at Mollie, can not be shipped/captured.', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order already completed at Mollie, can not be shipped/captured.');
-
- return;
- }
-
- if ($mollie_order->isPaid() || $mollie_order->isAuthorized()) {
- $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id)->shipAll();
- $message = _x('Order successfully updated to shipped at Mollie, capture of funds underway.', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order successfully updated to shipped at Mollie, capture of funds underway.');
-
- return;
- }
- $message = _x('Order not paid or authorized at Mollie yet, can not be shipped.', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order not paid or authorized at Mollie yet, can not be shipped.');
- } catch (ApiException $e) {
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Processing shipment & capture failed, error: ' . $e->getMessage());
- }
-
- return;
- }
-
- /**
- * Cancel an order at Mollie.
- *
- */
- public function cancelOrderAtMollie($order_id)
- {
- $order = wc_get_order($order_id);
-
- // Does WooCommerce order contain a Mollie payment?
- if (strstr($order->get_payment_method(), 'mollie_wc_gateway_') === false) {
- return;
- }
-
- // To disable automatic canceling of the Mollie order when a WooCommerce order status is updated to canceled,
- // store an option 'mollie-payments-for-woocommerce_disableCancelOrderAtMollie' with value 1
- if (get_option($this->pluginId . '_' . 'disableCancelOrderAtMollie', '0') === '1') {
- return;
- }
-
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Try to process cancelled order at Mollie.');
-
- $mollie_order_id = ( $mollie_order_id = $order->get_meta('_mollie_order_id', true) ) ? $mollie_order_id : false;
-
- if ($mollie_order_id === false) {
- $message = _x('Order contains Mollie payment method, but not a valid Mollie Order ID. Canceling order failed.', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order contains Mollie payment method, but not a valid Mollie Order ID. Canceling order failed.');
-
- return;
- }
-
- $orderStr = "ord_";
- if (substr($mollie_order_id, 0, strlen($orderStr)) !== $orderStr) {
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order uses Payment API, cannot cancel as order.');
-
- return;
- }
-
- $apiKey = $this->settingsHelper->getApiKey();
- try {
- // Get the order from the Mollie API
- $mollie_order = $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id);
-
- // Check that order is not already canceled at Mollie
- if ($mollie_order->isCanceled()) {
- $message = _x('Order already canceled at Mollie, can not be canceled again.', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order already canceled at Mollie, can not be canceled again.');
-
- return;
- }
-
- // Check that order has the correct status to be canceled
- if ($mollie_order->isCreated() || $mollie_order->isAuthorized() || $mollie_order->isShipping()) {
- $this->apiHelper->getApiClient($apiKey)->orders->get($mollie_order_id)->cancel();
- $message = _x('Order also cancelled at Mollie.', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message);
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order cancelled in WooCommerce, also cancelled at Mollie.');
-
- return;
- }
- $message = _x('Order could not be canceled at Mollie, because order status is ', 'Order note info', 'mollie-payments-for-woocommerce');
- $order->add_order_note($message . $mollie_order->status . '.');
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Order could not be canceled at Mollie, because order status is ' . $mollie_order->status . '.');
- } catch (ApiException $e) {
- $this->logger->debug(__METHOD__ . ' - ' . $order_id . ' - Updating order to canceled at Mollie failed, error: ' . $e->getMessage());
- }
-
- return;
- }
/**
* Add/remove scheduled action to cancel orders on expiration date
diff --git a/src/PluginApi/MolliePluginApi.php b/src/PluginApi/MolliePluginApi.php
new file mode 100644
index 00000000..4869b6a4
--- /dev/null
+++ b/src/PluginApi/MolliePluginApi.php
@@ -0,0 +1,123 @@
+capturePayment = $capturePayment;
+ $this->voidPayment = $voidPayment;
+ $this->mollieObject = $mollieObject;
+ }
+
+ /**
+ * Initializes the MolliePluginApi with necessary dependencies.
+ */
+ public static function init(
+ Closure $capturePayment,
+ Closure $voidPayment,
+ MollieObject $mollieObject
+ ): void {
+
+ if (self::$instance === null) {
+ self::$instance = new self(
+ $capturePayment,
+ $voidPayment,
+ $mollieObject
+ );
+ }
+ }
+
+ /**
+ * Returns the singleton instance of MolliePluginApi.
+ *
+ * @throws \LogicException If the API has not been initialized.
+ */
+ public static function getInstance(): self
+ {
+
+ if (self::$instance === null) {
+ throw new \LogicException('MolliePluginApi has not been initialized.');
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Captures the Mollie order for the given WooCommerce order.
+ * Logs the result of the operation.
+ *
+ * @param \WC_Order $wcOrder The WooCommerce order.
+ */
+ public function captureOrder(\WC_Order $wcOrder): void
+ {
+
+ ($this->capturePayment)($wcOrder->get_id());
+ }
+
+ /**
+ * Refunds the Mollie order for the given WooCommerce order.
+ *
+ * @param \WC_Order $wcOrder The WooCommerce order.
+ * @param float $amount The refund amount.
+ * @param string $reason The reason for the refund.
+ * @return \WP_Error|Refund The result of the refund operation.
+ */
+ public function refundOrder(\WC_Order $wcOrder, float $amount, string $reason = '')
+ {
+ $mollieRefund = null;
+ add_action('mollie-payments-for-woocommerce_refund_amount_created', function ($refund, $order, $amount) use (&$mollieRefund) {
+ $mollieRefund = $refund;
+ }, 10, 3);
+ $refundCreated = $this->mollieObject->processRefund($wcOrder->get_id(), $amount, $reason);
+ return $refundCreated ? $mollieRefund : new \WP_Error('mollie_refund_failed', __('Refund failed', 'mollie-payments-for-woocommerce'));
+ }
+
+ /**
+ * Voids the authorization for the given WooCommerce order.
+ * Logs the result of the operation.
+ *
+ * @param \WC_Order $wcOrder The WooCommerce order.
+ */
+ public function voidOrder(\WC_Order $wcOrder): void
+ {
+
+ ($this->voidPayment)($wcOrder->get_id());
+ }
+
+ /**
+ * Cancels the Order at Mollie and also in WooCommerce if was not already done.
+ * Logs the result of the operation.
+ *
+ * @param \WC_Order $wcOrder The WooCommerce order.
+ */
+ public function cancelOrder(string $orderId): void
+ {
+
+ $this->mollieObject->cancelOrderAtMollie($orderId);
+ }
+
+ /**
+ * Ship all order lines and capture an order at Mollie.
+ * Logs the result of the operation.
+ *
+ * @param string $orderId The WooCommerce order ID.
+ */
+ public function shipOrderAndCapture(string $orderId): void
+ {
+ $this->mollieObject->shipAndCaptureOrderAtMollie($orderId);
+ }
+}
diff --git a/src/PluginApi/PluginApiModule.php b/src/PluginApi/PluginApiModule.php
new file mode 100644
index 00000000..c63b35ed
--- /dev/null
+++ b/src/PluginApi/PluginApiModule.php
@@ -0,0 +1,30 @@
+get(CapturePayment::class),
+ $container->get(VoidPayment::class),
+ $container->get(MollieObject::class)
+ );
+ return true;
+ }
+}
diff --git a/src/Subscription/MaybeFixSubscription.php b/src/Subscription/MaybeFixSubscription.php
index 9d5ae1ca..5c6c147b 100644
--- a/src/Subscription/MaybeFixSubscription.php
+++ b/src/Subscription/MaybeFixSubscription.php
@@ -4,6 +4,8 @@
namespace Mollie\WooCommerce\Subscription;
+use Mollie\WooCommerce\Gateway\MolliePaymentGateway;
+
class MaybeFixSubscription
{
public function maybeFix()
@@ -39,7 +41,7 @@ public function retrieveAndFixBrokenSubscriptions()
$parent = $subscription->get_parent();
if ($parent) {
$subscription->update_meta_data('_mollie_customer_id', $parent->get_meta('_mollie_customer_id'));
- $subscription->update_meta_data('_mollie_order_id', $parent->get_meta('_mollie_order_id'));
+ $subscription->update_meta_data(MolliePaymentGateway::ORDER_ID_META_KEY, $parent->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY));
$subscription->update_meta_data('_mollie_payment_id', $parent->get_meta('_mollie_payment_id'));
$subscription->update_meta_data('_mollie_payment_mode', $parent->get_meta('_mollie_payment_mode'));
$subscription->save();
diff --git a/src/Subscription/MollieSubscriptionGateway.php b/src/Subscription/MollieSubscriptionGateway.php
index ec3d17fa..8d003cf3 100644
--- a/src/Subscription/MollieSubscriptionGateway.php
+++ b/src/Subscription/MollieSubscriptionGateway.php
@@ -748,7 +748,7 @@ protected function initialPaymentUsedOrderAPI($subscriptionParentOrder): bool
if (!$subscriptionParentOrder) {
return false;
}
- $orderIdMeta = $subscriptionParentOrder->get_meta('_mollie_order_id');
+ $orderIdMeta = $subscriptionParentOrder->get_meta(MolliePaymentGateway::ORDER_ID_META_KEY);
$parentOrderMeta = $orderIdMeta ?: PaymentService::PAYMENT_METHOD_TYPE_PAYMENT;
diff --git a/tests/php/Functional/OrderFunctions/MollieOrderTest.php b/tests/php/Functional/OrderFunctions/MollieOrderTest.php
new file mode 100644
index 00000000..6ff51748
--- /dev/null
+++ b/tests/php/Functional/OrderFunctions/MollieOrderTest.php
@@ -0,0 +1,116 @@
+createMock(WC_Order::class);
+ $mockOrder->method('get_id')->willReturn('123');
+ $capturePaymentMock = $this->getMockBuilder(stdClass::class)
+ ->addMethods(['__invoke'])
+ ->getMock();
+ $capturePaymentClosure = function ($orderId) use ($capturePaymentMock) {
+ $capturePaymentMock->__invoke($orderId);
+ };
+ $capturePaymentMock->expects($this->once())
+ ->method('__invoke')
+ ->with($this->equalTo(123));
+
+ $voidPaymentMock = function() {};
+ $mollieObjectMock = $this->createMock(MollieObject::class);
+
+ MolliePluginApi::init($capturePaymentClosure, $voidPaymentMock, $mollieObjectMock);
+ mollie_capture_order($mockOrder);
+ }
+
+ /**
+ * @runInSeparateProcess
+ */
+ public function test_voidOrder_invokes_voidPayment_with_correct_order() {
+ $mockOrder = $this->createMock(WC_Order::class);
+ $mockOrder->method('get_id')->willReturn('123');
+ $voidPaymentMock = $this->getMockBuilder(stdClass::class)
+ ->addMethods(['__invoke'])
+ ->getMock();
+ $voidPaymentClosure = function ($orderId) use ($voidPaymentMock) {
+ $voidPaymentMock->__invoke($orderId);
+ };
+ $voidPaymentMock->expects($this->once())
+ ->method('__invoke')
+ ->with($this->equalTo(123));
+
+ $capturePaymentMock = function() {};
+ $mollieObjectMock = $this->createMock(MollieObject::class);
+
+ MolliePluginApi::init($capturePaymentMock, $voidPaymentClosure, $mollieObjectMock);
+ mollie_void_order($mockOrder);
+ }
+
+ /**
+ * @runInSeparateProcess
+ */
+ public function test_refundOrder_with_correct_order() {
+ $mockOrder = $this->createMock(WC_Order::class);
+ $mockOrder->method('get_id')->willReturn('123');
+ $voidPaymentMock = function() {};
+ $capturePaymentMock = function() {};
+ $mollieObjectMock = $this->createConfiguredMock(MollieObject::class, [
+ 'processRefund' => true
+ ]);
+ $mollieObjectMock->expects($this->once())->method('processRefund')->with('123', 10.0, 'reason');
+
+ MolliePluginApi::init($capturePaymentMock, $voidPaymentMock, $mollieObjectMock);
+ mollie_refund_order($mockOrder, 10.0, 'reason');
+ }
+
+ /**
+ * @runInSeparateProcess
+ */
+ public function test_cancelOrder_with_correct_order() {
+ $mockOrder = $this->createMock(WC_Order::class);
+ $mockOrder->method('get_id')->willReturn('123');
+ $voidPaymentMock = function() {};
+ $capturePaymentMock = function() {};
+ $mollieObjectMock = $this->createConfiguredMock(MollieObject::class, [
+ 'cancelOrderAtMollie' => true
+ ]);
+ $mollieObjectMock->expects($this->once())->method('cancelOrderAtMollie')->with('123');
+
+ MolliePluginApi::init($capturePaymentMock, $voidPaymentMock, $mollieObjectMock);
+ mollie_cancel_order($mockOrder);
+ }
+
+ /**
+ * @runInSeparateProcess
+ */
+ public function test_shipOrder_with_correct_order() {
+ $mockOrder = $this->createMock(WC_Order::class);
+ $mockOrder->method('get_id')->willReturn('123');
+ $voidPaymentMock = function() {};
+ $capturePaymentMock = function() {};
+ $mollieObjectMock = $this->createConfiguredMock(MollieObject::class, [
+ 'shipAndCaptureOrderAtMollie' => true
+ ]);
+ $mollieObjectMock->expects($this->once())->method('shipAndCaptureOrderAtMollie')->with('123');
+
+ MolliePluginApi::init($capturePaymentMock, $voidPaymentMock, $mollieObjectMock);
+ mollie_ship_order($mockOrder);
+ }
+}