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); + } +}