Commit e97533040b for woocommerce
commit e97533040b60009d345113cedac6d3ba7baf92f9
Author: Wesley Rosa <wesleyjrosa@gmail.com>
Date: Mon Jan 5 09:13:23 2026 -0300
PayPal Standard Refactor 5: Moving the Webhook Handler class to the src folder (#62630)
* Moving the webhook handler class to the src folder
* Add changefile(s) from automation for the following project(s): woocommerce
* Fix PHPStan issues
* Fix PHPStan issues
* Fix PHPStan and lint issues
* Adding missing param types
* Fix test class package name
* Fix test class package name
* Update plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com>
* Update plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php
Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com>
* Update plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php
Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com>
* Adding the deprecation warning method call
* Update plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
Co-authored-by: Malith Senaweera <6216000+malithsen@users.noreply.github.com>
* Renaming the changelog file
* Add changefile(s) from automation for the following project(s): woocommerce
* PHPStan baseline update
* PHPStan baseline update
* Removing duplicate changelog file
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com>
Co-authored-by: Malith Senaweera <6216000+malithsen@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/62630-refactor-paypal-standard-5-webhook-handler-class b/plugins/woocommerce/changelog/62630-refactor-paypal-standard-5-webhook-handler-class
new file mode 100644
index 0000000000..59a63992fa
--- /dev/null
+++ b/plugins/woocommerce/changelog/62630-refactor-paypal-standard-5-webhook-handler-class
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Deprecate WC_Gateway_Paypal_Webhook_Handler class in favor of Automattic\WooCommerce\Gateways\PayPal\WebhookHandler class.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php
index 23855b7790..b26414d936 100644
--- a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php
+++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php
@@ -259,9 +259,9 @@ class WC_Gateway_Paypal_Request {
*
* This method authorizes or captures a PayPal payment and updates the order status.
*
- * @param WC_Order $order Order object.
- * @param string $action_url The URL to authorize or capture the payment.
- * @param string $action The action to perform. Either 'authorize' or 'capture'.
+ * @param WC_Order $order Order object.
+ * @param string|null $action_url The URL to authorize or capture the payment.
+ * @param string $action The action to perform. Either 'authorize' or 'capture'.
* @return void
* @throws Exception If the PayPal payment authorization or capture fails.
*/
diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
index f1f6912abb..b4c57321d3 100644
--- a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
+++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
@@ -3,16 +3,18 @@
* Class WC_Gateway_Paypal_Webhook_Handler file.
*
* @package WooCommerce\Gateways
+ *
+ * @deprecated 10.5.0 Deprecated in favor of Automattic\WooCommerce\Gateways\PayPal\WebhookHandler
*/
declare(strict_types=1);
-use Automattic\WooCommerce\Enums\OrderStatus;
-
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
+use Automattic\WooCommerce\Gateways\PayPal\WebhookHandler as PayPalWebhookHandler;
+
if ( ! class_exists( 'WC_Gateway_Paypal_Helper' ) ) {
require_once __DIR__ . '/class-wc-gateway-paypal-helper.php';
}
@@ -23,229 +25,43 @@ if ( ! class_exists( 'WC_Gateway_Paypal_Request' ) ) {
/**
* Handles webhook events.
+ *
+ * @deprecated 10.5.0 Deprecated in favor of Automattic\WooCommerce\Gateways\PayPal\WebhookHandler
*/
class WC_Gateway_Paypal_Webhook_Handler {
/**
- * Process the webhook event.
+ * The delegated webhook handler instance.
*
- * @param WP_REST_Request $request The request object.
+ * @var PayPalWebhookHandler
*/
- public function process_webhook( WP_REST_Request $request ) {
- $data = $request->get_json_params();
- if ( ! is_array( $data ) || empty( $data['event_type'] ) || empty( $data['resource'] ) ) {
- WC_Gateway_Paypal::log( 'Invalid PayPal webhook payload: ' . wc_print_r( $data, true ) );
- return;
- }
-
- WC_Gateway_Paypal::log( 'Webhook received: ' . wc_print_r( WC_Gateway_Paypal_Helper::redact_data( $data ), true ) );
-
- switch ( $data['event_type'] ) {
- case 'CHECKOUT.ORDER.APPROVED':
- $this->process_checkout_order_approved( $data );
- break;
- case 'PAYMENT.CAPTURE.PENDING':
- $this->process_payment_capture_pending( $data );
- break;
- case 'PAYMENT.CAPTURE.COMPLETED':
- $this->process_payment_capture_completed( $data );
- break;
- case 'PAYMENT.AUTHORIZATION.CREATED':
- $this->process_payment_authorization_created( $data );
- break;
- default:
- WC_Gateway_Paypal::log( 'Unhandled PayPal webhook event: ' . wc_print_r( WC_Gateway_Paypal_Helper::redact_data( $data ), true ) );
- break;
- }
- }
+ private PayPalWebhookHandler $webhook_handler;
/**
- * Process the CHECKOUT.ORDER.APPROVED webhook event.
- *
- * @param array $event The webhook event data.
+ * Constructor.
*/
- private function process_checkout_order_approved( $event ) {
- $custom_id = $event['resource']['purchase_units'][0]['custom_id'] ?? '';
- $order = WC_Gateway_Paypal_Helper::get_wc_order_from_paypal_custom_id( $custom_id );
- if ( ! $order ) {
- WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
- return;
- }
-
- // Skip if the payment is already processed.
- $paypal_status = $order->get_meta( '_paypal_status', true );
- if ( in_array( $paypal_status, array( WC_Gateway_Paypal_Constants::STATUS_COMPLETED, WC_Gateway_Paypal_Constants::STATUS_APPROVED ), true ) ) {
- return;
- }
-
- $status = $event['resource']['status'] ?? null;
- $paypal_order_id = $event['resource']['id'] ?? null;
- if ( WC_Gateway_Paypal_Constants::STATUS_APPROVED === $status ) {
- WC_Gateway_Paypal::log( 'PayPal payment approved. Order ID: ' . $order->get_id() );
- $order->update_meta_data( '_paypal_status', $status );
- $order->add_order_note(
- sprintf(
- /* translators: %1$s: PayPal order ID */
- __( 'PayPal payment approved. PayPal Order ID: %1$s', 'woocommerce' ),
- $paypal_order_id
- )
- );
- $order->save();
-
- // Update the addresses in the order with the addresses from the PayPal order details.
- WC_Gateway_Paypal_Helper::update_addresses_in_order( $order, $event['resource'] );
-
- // Authorize or capture the payment after approval.
- $paypal_intent = $event['resource']['intent'] ?? null;
- $links = $event['resource']['links'] ?? null;
- $action = WC_Gateway_Paypal_Constants::INTENT_CAPTURE === $paypal_intent ? WC_Gateway_Paypal_Constants::PAYMENT_ACTION_CAPTURE : WC_Gateway_Paypal_Constants::PAYMENT_ACTION_AUTHORIZE;
- $this->authorize_or_capture_payment( $order, $links, $action );
- } else {
- // This is unexpected for a CHECKOUT.ORDER.APPROVED event.
- WC_Gateway_Paypal::log( 'PayPal payment approval failed. Order ID: ' . $order->get_id() . ' Status: ' . $status );
- $order->add_order_note(
- sprintf(
- /* translators: %1$s: PayPal order ID, %2$s: Status */
- __( 'PayPal payment approval failed. PayPal Order ID: %1$s. Status: %2$s', 'woocommerce' ),
- $paypal_order_id,
- $status
- )
- );
- }
- }
-
- /**
- * Process the PAYMENT.CAPTURE.COMPLETED webhook event.
- *
- * @param array $event The webhook event data.
- */
- private function process_payment_capture_completed( $event ) {
- $custom_id = $event['resource']['custom_id'] ?? '';
- $order = WC_Gateway_Paypal_Helper::get_wc_order_from_paypal_custom_id( $custom_id );
- if ( ! $order ) {
- WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
- return;
- }
-
- // Skip if the payment is already processed.
- if ( WC_Gateway_Paypal_Constants::STATUS_COMPLETED === $order->get_meta( '_paypal_status', true ) ) {
- return;
- }
-
- $transaction_id = $event['resource']['id'] ?? null;
- $status = $event['resource']['status'] ?? null;
- $order->set_transaction_id( $transaction_id );
- $order->update_meta_data( '_paypal_capture_id', $transaction_id );
- $order->update_meta_data( '_paypal_status', $status );
- $order->payment_complete();
- $order->add_order_note(
- sprintf(
- /* translators: %1$s: Transaction ID */
- __( 'PayPal payment captured. Transaction ID: %1$s.', 'woocommerce' ),
- $transaction_id
- )
- );
- $order->save();
+ public function __construct() {
+ $this->webhook_handler = new PayPalWebhookHandler();
}
/**
- * Process the PAYMENT.CAPTURE.PENDING webhook event.
+ * Process the webhook event.
*
- * @param array $event The webhook event data.
- */
- private function process_payment_capture_pending( $event ) {
- $custom_id = $event['resource']['custom_id'] ?? '';
- $order = WC_Gateway_Paypal_Helper::get_wc_order_from_paypal_custom_id( $custom_id );
- if ( ! $order ) {
- WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
- return;
- }
-
- // Skip if the payment is already processed.
- if ( WC_Gateway_Paypal_Constants::STATUS_COMPLETED === $order->get_meta( '_paypal_status', true ) ) {
- return;
- }
-
- $transaction_id = $event['resource']['id'] ?? null;
- $status = $event['resource']['status'] ?? null;
- $reason = $event['resource']['status_details']['reason'] ?? 'Unknown';
- $order->set_transaction_id( $transaction_id );
- $order->update_meta_data( '_paypal_capture_id', $transaction_id );
- $order->update_meta_data( '_paypal_status', $status );
- /* translators: %s: reason */
- $order->update_status( OrderStatus::ON_HOLD, sprintf( __( 'Payment pending (reason: %s).', 'woocommerce' ), $reason ) );
- $order->save();
- }
-
- /**
- * Process the PAYMENT.AUTHORIZATION.CREATED webhook event.
+ * @deprecated 10.5.0 Use Automattic\WooCommerce\Gateways\PayPal\WebhookHandler::process_webhook() instead. This method will be removed in 11.0.0.
*
- * @param array $event The webhook event data.
- */
- private function process_payment_authorization_created( $event ) {
- $custom_id = $event['resource']['custom_id'] ?? '';
- $order = WC_Gateway_Paypal_Helper::get_wc_order_from_paypal_custom_id( $custom_id );
- if ( ! $order ) {
- WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
- return;
- }
-
- // Skip if the payment is already processed.
- if ( WC_Gateway_Paypal_Constants::STATUS_COMPLETED === $order->get_meta( '_paypal_status', true ) ) {
- return;
- }
-
- $transaction_id = $event['resource']['id'] ?? null;
- $order->set_transaction_id( $transaction_id );
- $order->update_meta_data( '_paypal_authorization_id', $transaction_id );
- $order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED );
- $order->add_order_note(
- sprintf(
- /* translators: %1$s: Transaction ID */
- __( 'PayPal payment authorized. Transaction ID: %1$s. Change payment status to processing or complete to capture funds.', 'woocommerce' ),
- $transaction_id
- )
- );
- $order->update_status( OrderStatus::ON_HOLD );
- $order->save();
- }
-
- /**
- * Capture the payment.
+ * @param WP_REST_Request $request The request object.
*
- * @param WC_Order $order The order object.
- * @param array $links The links from the webhook event.
- * @param string $action The action to perform (capture or authorize).
* @return void
- */
- private function authorize_or_capture_payment( $order, $links, $action ) {
- $action_url = $this->get_action_url( $links, $action );
-
- $payment_gateways = WC()->payment_gateways()->payment_gateways();
- if ( ! isset( $payment_gateways['paypal'] ) ) {
- WC_Gateway_Paypal::log( 'PayPal gateway is not available.' );
- return;
- }
- $gateway = $payment_gateways['paypal'];
- $paypal_request = new WC_Gateway_Paypal_Request( $gateway );
- $paypal_request->authorize_or_capture_payment( $order, $action_url, $action );
- }
-
- /**
- * Get the action URL from the links.
*
- * @param array $links The links from the webhook event.
- * @param string $action The action to perform (capture or authorize).
- * @return string|null
+ * @deprecated 10.5.0 Deprecated in favor of Automattic\WooCommerce\Gateways\PayPal\WebhookHandler::process_webhook
*/
- private function get_action_url( $links, $action ) {
- $action_url = null;
- foreach ( $links as $link ) {
- if ( $action === $link['rel'] && 'POST' === $link['method'] && filter_var( $link['href'], FILTER_VALIDATE_URL ) ) {
- $action_url = esc_url_raw( $link['href'] );
- break;
- }
- }
- return $action_url;
+ public function process_webhook( WP_REST_Request $request ) {
+ wc_deprecated_function(
+ __METHOD__,
+ '10.5.0',
+ PayPalWebhookHandler::class . '::process_webhook()'
+ );
+
+ $this->webhook_handler->process_webhook( $request );
}
}
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-webhooks-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-webhooks-controller.php
index bcacb8cd1b..13fda53451 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-webhooks-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-webhooks-controller.php
@@ -12,6 +12,9 @@
declare(strict_types=1);
defined( 'ABSPATH' ) || exit;
+
+use Automattic\WooCommerce\Gateways\PayPal\WebhookHandler as PayPalWebhookHandler;
+
/**
* REST API PayPal webhook handler controller class.
*
@@ -80,8 +83,7 @@ class WC_REST_Paypal_Webhooks_Controller extends WC_REST_Controller {
* @return WP_REST_Response The response object.
*/
public function process_webhook( WP_REST_Request $request ) {
- include_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php';
- $webhook_handler = new WC_Gateway_Paypal_Webhook_Handler();
+ $webhook_handler = new PayPalWebhookHandler();
try {
$webhook_handler->process_webhook( $request );
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 64c0c612f5..252aa215c8 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -25164,48 +25164,12 @@ parameters:
count: 2
path: includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php
- -
- message: '#^Method WC_Gateway_Paypal_Webhook_Handler\:\:process_checkout_order_approved\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
-
- -
- message: '#^Method WC_Gateway_Paypal_Webhook_Handler\:\:process_payment_authorization_created\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
-
- -
- message: '#^Method WC_Gateway_Paypal_Webhook_Handler\:\:process_payment_capture_completed\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
-
- -
- message: '#^Method WC_Gateway_Paypal_Webhook_Handler\:\:process_payment_capture_pending\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
-
- -
- message: '#^Method WC_Gateway_Paypal_Webhook_Handler\:\:process_webhook\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
-
-
message: '#^Method WC_Gateway_Paypal_Webhook_Handler\:\:process_webhook\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
- -
- message: '#^Parameter \#2 \$action_url of method WC_Gateway_Paypal_Request\:\:authorize_or_capture_payment\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php
-
-
message: '#^Call to an undefined method object\:\:set_variation\(\)\.$#'
identifier: method.notFound
@@ -34020,12 +33984,6 @@ parameters:
count: 1
path: includes/rest-api/Controllers/Version3/class-wc-rest-paypal-webhooks-controller.php
- -
- message: '#^Constant WC_ABSPATH not found\.$#'
- identifier: constant.notFound
- count: 1
- path: includes/rest-api/Controllers/Version3/class-wc-rest-paypal-webhooks-controller.php
-
-
message: '#^Method WC_REST_Paypal_Webhooks_Controller\:\:process_webhook\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
identifier: missingType.generics
@@ -64233,6 +64191,18 @@ parameters:
count: 1
path: src/Gateways/PayPal/TransactAccountManager.php
+ -
+ message: '#^Constant WC_ABSPATH not found\.$#'
+ identifier: constant.notFound
+ count: 1
+ path: src/Gateways/PayPal/WebhookHandler.php
+
+ -
+ message: '#^Method Automattic\\WooCommerce\\Gateways\\PayPal\\WebhookHandler\:\:process_webhook\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
+ identifier: missingType.generics
+ count: 1
+ path: src/Gateways/PayPal/WebhookHandler.php
+
-
message: '#^Access to an undefined property object\:\:\$payload\.$#'
identifier: property.notFound
diff --git a/plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php b/plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php
new file mode 100644
index 0000000000..0e3ab2d5f8
--- /dev/null
+++ b/plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php
@@ -0,0 +1,269 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Gateways\PayPal;
+
+use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
+use Automattic\WooCommerce\Gateways\PayPal\Helper as PayPalHelper;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+if ( ! class_exists( 'WC_Gateway_Paypal_Request' ) ) {
+ require_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php';
+}
+
+/**
+ * Class WebhookHandler file.
+ *
+ * Handles webhook events.
+ *
+ * @since 10.5.0
+ */
+class WebhookHandler {
+
+ /**
+ * Process the webhook event.
+ *
+ * @since 10.5.0
+ *
+ * @param \WP_REST_Request $request The request object.
+ * @return void
+ */
+ public function process_webhook( \WP_REST_Request $request ): void {
+ $data = $request->get_json_params();
+ if ( ! is_array( $data ) || empty( $data['event_type'] ) || empty( $data['resource'] ) ) {
+ \WC_Gateway_Paypal::log( 'Invalid PayPal webhook payload: ' . wc_print_r( $data, true ) );
+ return;
+ }
+
+ \WC_Gateway_Paypal::log( 'Webhook received: ' . wc_print_r( PayPalHelper::redact_data( $data ), true ) );
+
+ switch ( $data['event_type'] ) {
+ case 'CHECKOUT.ORDER.APPROVED':
+ $this->process_checkout_order_approved( $data );
+ break;
+ case 'PAYMENT.CAPTURE.PENDING':
+ $this->process_payment_capture_pending( $data );
+ break;
+ case 'PAYMENT.CAPTURE.COMPLETED':
+ $this->process_payment_capture_completed( $data );
+ break;
+ case 'PAYMENT.AUTHORIZATION.CREATED':
+ $this->process_payment_authorization_created( $data );
+ break;
+ default:
+ \WC_Gateway_Paypal::log( 'Unhandled PayPal webhook event: ' . wc_print_r( PayPalHelper::redact_data( $data ), true ) );
+ break;
+ }
+ }
+
+ /**
+ * Process the CHECKOUT.ORDER.APPROVED webhook event.
+ *
+ * @since 10.5.0
+ *
+ * @param array $event The webhook event data.
+ * @return void
+ */
+ private function process_checkout_order_approved( array $event ): void {
+ $custom_id = $event['resource']['purchase_units'][0]['custom_id'] ?? '';
+ $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id );
+ if ( ! $order ) {
+ \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
+ return;
+ }
+
+ // Skip if the payment is already processed.
+ $paypal_status = $order->get_meta( '_paypal_status', true );
+ if ( in_array( $paypal_status, array( PayPalConstants::STATUS_COMPLETED, PayPalConstants::STATUS_APPROVED ), true ) ) {
+ return;
+ }
+
+ $status = $event['resource']['status'] ?? null;
+ $paypal_order_id = $event['resource']['id'] ?? null;
+ if ( PayPalConstants::STATUS_APPROVED === $status ) {
+ \WC_Gateway_Paypal::log( 'PayPal payment approved. Order ID: ' . $order->get_id() );
+ $order->update_meta_data( '_paypal_status', $status );
+ $order->add_order_note(
+ sprintf(
+ /* translators: %1$s: PayPal order ID */
+ __( 'PayPal payment approved. PayPal Order ID: %1$s', 'woocommerce' ),
+ $paypal_order_id
+ )
+ );
+ $order->save();
+
+ // Update the addresses in the order with the addresses from the PayPal order details.
+ PayPalHelper::update_addresses_in_order( $order, $event['resource'] );
+
+ // Authorize or capture the payment after approval.
+ $paypal_intent = $event['resource']['intent'] ?? null;
+ $links = $event['resource']['links'] ?? null;
+ $action = PayPalConstants::INTENT_CAPTURE === $paypal_intent ? PayPalConstants::PAYMENT_ACTION_CAPTURE : PayPalConstants::PAYMENT_ACTION_AUTHORIZE;
+ $this->authorize_or_capture_payment( $order, $links, $action );
+ } else {
+ // This is unexpected for a CHECKOUT.ORDER.APPROVED event.
+ \WC_Gateway_Paypal::log( 'PayPal payment approval failed. Order ID: ' . $order->get_id() . ' Status: ' . $status );
+ $order->add_order_note(
+ sprintf(
+ /* translators: %1$s: PayPal order ID, %2$s: Status */
+ __( 'PayPal payment approval failed. PayPal Order ID: %1$s. Status: %2$s', 'woocommerce' ),
+ $paypal_order_id,
+ $status
+ )
+ );
+ }
+ }
+
+ /**
+ * Process the PAYMENT.CAPTURE.COMPLETED webhook event.
+ *
+ * @since 10.5.0
+ *
+ * @param array $event The webhook event data.
+ * @return void
+ */
+ private function process_payment_capture_completed( array $event ): void {
+ $custom_id = $event['resource']['custom_id'] ?? '';
+ $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id );
+ if ( ! $order ) {
+ \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
+ return;
+ }
+
+ // Skip if the payment is already processed.
+ if ( PayPalConstants::STATUS_COMPLETED === $order->get_meta( '_paypal_status', true ) ) {
+ return;
+ }
+
+ $transaction_id = $event['resource']['id'] ?? null;
+ $status = $event['resource']['status'] ?? null;
+ $order->set_transaction_id( $transaction_id );
+ $order->update_meta_data( '_paypal_capture_id', $transaction_id );
+ $order->update_meta_data( '_paypal_status', $status );
+ $order->payment_complete();
+ $order->add_order_note(
+ sprintf(
+ /* translators: %1$s: Transaction ID */
+ __( 'PayPal payment captured. Transaction ID: %1$s.', 'woocommerce' ),
+ $transaction_id
+ )
+ );
+ $order->save();
+ }
+
+ /**
+ * Process the PAYMENT.CAPTURE.PENDING webhook event.
+ *
+ * @since 10.5.0
+ *
+ * @param array $event The webhook event data.
+ * @return void
+ */
+ private function process_payment_capture_pending( array $event ): void {
+ $custom_id = $event['resource']['custom_id'] ?? '';
+ $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id );
+ if ( ! $order ) {
+ \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
+ return;
+ }
+
+ // Skip if the payment is already processed.
+ if ( PayPalConstants::STATUS_COMPLETED === $order->get_meta( '_paypal_status', true ) ) {
+ return;
+ }
+
+ $transaction_id = $event['resource']['id'] ?? null;
+ $status = $event['resource']['status'] ?? null;
+ $reason = $event['resource']['status_details']['reason'] ?? 'Unknown';
+ $order->set_transaction_id( $transaction_id );
+ $order->update_meta_data( '_paypal_capture_id', $transaction_id );
+ $order->update_meta_data( '_paypal_status', $status );
+ /* translators: %s: reason */
+ $order->update_status( OrderStatus::ON_HOLD, sprintf( __( 'Payment pending (reason: %s).', 'woocommerce' ), $reason ) );
+ $order->save();
+ }
+
+ /**
+ * Process the PAYMENT.AUTHORIZATION.CREATED webhook event.
+ *
+ * @since 10.5.0
+ *
+ * @param array $event The webhook event data.
+ * @return void
+ */
+ private function process_payment_authorization_created( array $event ): void {
+ $custom_id = $event['resource']['custom_id'] ?? '';
+ $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id );
+ if ( ! $order ) {
+ \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) );
+ return;
+ }
+
+ // Skip if the payment is already processed.
+ if ( PayPalConstants::STATUS_COMPLETED === $order->get_meta( '_paypal_status', true ) ) {
+ return;
+ }
+
+ $transaction_id = $event['resource']['id'] ?? null;
+ $order->set_transaction_id( $transaction_id );
+ $order->update_meta_data( '_paypal_authorization_id', $transaction_id );
+ $order->update_meta_data( '_paypal_status', PayPalConstants::STATUS_AUTHORIZED );
+ $order->add_order_note(
+ sprintf(
+ /* translators: %1$s: Transaction ID */
+ __( 'PayPal payment authorized. Transaction ID: %1$s. Change payment status to processing or complete to capture funds.', 'woocommerce' ),
+ $transaction_id
+ )
+ );
+ $order->update_status( OrderStatus::ON_HOLD );
+ $order->save();
+ }
+
+ /**
+ * Capture the payment.
+ *
+ * @since 10.5.0
+ *
+ * @param \WC_Order $order The order object.
+ * @param array $links The links from the webhook event.
+ * @param string $action The action to perform (capture or authorize).
+ * @return void
+ */
+ private function authorize_or_capture_payment( \WC_Order $order, array $links, string $action ): void {
+ $action_url = $this->get_action_url( $links, $action );
+
+ $payment_gateways = WC()->payment_gateways()->payment_gateways();
+ if ( ! isset( $payment_gateways['paypal'] ) ) {
+ \WC_Gateway_Paypal::log( 'PayPal gateway is not available.' );
+ return;
+ }
+ $gateway = $payment_gateways['paypal'];
+ $paypal_request = new \WC_Gateway_Paypal_Request( $gateway );
+ $paypal_request->authorize_or_capture_payment( $order, $action_url, $action );
+ }
+
+ /**
+ * Get the action URL from the links.
+ *
+ * @since 10.5.0
+ *
+ * @param array $links The links from the webhook event.
+ * @param string $action The action to perform (capture or authorize).
+ * @return string|null
+ */
+ private function get_action_url( array $links, string $action ): ?string {
+ $action_url = null;
+ foreach ( $links as $link ) {
+ if ( $action === $link['rel'] && 'POST' === $link['method'] && filter_var( $link['href'], FILTER_VALIDATE_URL ) ) {
+ $action_url = esc_url_raw( $link['href'] );
+ break;
+ }
+ }
+ return $action_url;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-webhook-handler-test.php b/plugins/woocommerce/tests/php/src/Gateways/PayPal/WebhookHandlerTest.php
similarity index 84%
rename from plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-webhook-handler-test.php
rename to plugins/woocommerce/tests/php/src/Gateways/PayPal/WebhookHandlerTest.php
index e23f26adf8..33368ed439 100644
--- a/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-webhook-handler-test.php
+++ b/plugins/woocommerce/tests/php/src/Gateways/PayPal/WebhookHandlerTest.php
@@ -1,43 +1,48 @@
<?php
/**
- * Unit tests for WC_Gateway_Paypal_Webhook_Handler class.
+ * Unit tests for Automattic\WooCommerce\Gateways\PayPal\WebhookHandler class.
*
- * @package WooCommerce\Tests\Paypal.
+ * @package WooCommerce\Tests\Gateways\Paypal
*/
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited -- Required for testing WordPress globals
declare(strict_types=1);
-require_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-webhook-handler.php';
+namespace Automattic\WooCommerce\Tests\Gateways\PayPal;
+
+use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
+use Automattic\WooCommerce\Gateways\PayPal\WebhookHandler as PayPalWebhookHandler;
/**
- * Class WC_Gateway_Paypal_Webhook_Handler_Test.
+ * Class WebhookHandlerTest.
*/
-class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
+class WebhookHandlerTest extends \WC_Unit_Test_Case {
/**
* The webhook handler instance.
*
- * @var WC_Gateway_Paypal_Webhook_Handler
+ * @var PayPalWebhookHandler
*/
private $webhook_handler;
/**
* The mock request instance.
*
- * @var WP_REST_Request
+ * @var \WP_REST_Request
*/
private $mock_request;
/**
* Set up the test environment.
+ *
+ * @return void
*/
public function setUp(): void {
parent::setUp();
- $this->webhook_handler = new WC_Gateway_Paypal_Webhook_Handler();
- $this->mock_request = $this->createMock( WP_REST_Request::class );
+ $this->webhook_handler = new PayPalWebhookHandler();
+ $this->mock_request = $this->createMock( \WP_REST_Request::class );
// Prevent real network calls to PayPal during tests.
add_filter( 'pre_http_request', array( $this, 'mock_paypal_http_response' ) );
@@ -45,6 +50,8 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
/**
* Tear down the test environment.
+ *
+ * @return void
*/
public function tearDown(): void {
remove_filter( 'pre_http_request', array( $this, 'mock_paypal_http_response' ) );
@@ -55,16 +62,20 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
/**
* Mock HTTP calls to PayPal endpoints in tests.
+ *
+ * @return array
*/
- public function mock_paypal_http_response() {
+ public function mock_paypal_http_response(): array {
return array( 'response' => array( 'code' => 200 ) );
}
/**
* Test process_checkout_order_approved with valid data.
+ *
+ * @return void
*/
- public function test_process_checkout_order_approved_with_valid_data() {
- $test_order = WC_Helper_Order::create_order();
+ public function test_process_checkout_order_approved_with_valid_data(): void {
+ $test_order = \WC_Helper_Order::create_order();
$test_order->set_payment_method( 'paypal' );
$test_order->save();
@@ -110,11 +121,13 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
/**
* Test process_checkout_order_approved skips already processed orders.
+ *
+ * @return void
*/
- public function test_process_checkout_order_approved_skips_already_processed() {
- $test_order = WC_Helper_Order::create_order();
+ public function test_process_checkout_order_approved_skips_already_processed(): void {
+ $test_order = \WC_Helper_Order::create_order();
$test_order->set_payment_method( 'paypal' );
- $test_order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_COMPLETED );
+ $test_order->update_meta_data( '_paypal_status', PayPalConstants::STATUS_COMPLETED );
$test_order->save();
$custom_id_data = array(
@@ -151,9 +164,11 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
/**
* Test process_payment_capture_completed with valid data.
+ *
+ * @return void
*/
- public function test_process_payment_capture_completed_with_valid_data() {
- $test_order = WC_Helper_Order::create_order();
+ public function test_process_payment_capture_completed_with_valid_data(): void {
+ $test_order = \WC_Helper_Order::create_order();
$test_order->set_payment_method( 'paypal' );
$test_order->save();
@@ -187,11 +202,13 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
/**
* Test process_payment_capture_completed skips already processed orders.
+ *
+ * @return void
*/
- public function test_process_payment_capture_completed_skips_already_processed() {
- $test_order = WC_Helper_Order::create_order();
+ public function test_process_payment_capture_completed_skips_already_processed(): void {
+ $test_order = \WC_Helper_Order::create_order();
$test_order->set_payment_method( 'paypal' );
- $test_order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_COMPLETED );
+ $test_order->update_meta_data( '_paypal_status', PayPalConstants::STATUS_COMPLETED );
$test_order->save();
$custom_id_data = array(
@@ -224,9 +241,11 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
/**
* Test process_payment_authorization_created with valid data.
+ *
+ * @return void
*/
- public function test_process_payment_authorization_created_with_valid_data() {
- $test_order = WC_Helper_Order::create_order();
+ public function test_process_payment_authorization_created_with_valid_data(): void {
+ $test_order = \WC_Helper_Order::create_order();
$test_order->set_payment_method( 'paypal' );
$test_order->save();
@@ -261,11 +280,13 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
/**
* Test process_payment_authorization_created skips already processed orders.
+ *
+ * @return void
*/
- public function test_process_payment_authorization_created_skips_already_processed() {
- $test_order = WC_Helper_Order::create_order();
+ public function test_process_payment_authorization_created_skips_already_processed(): void {
+ $test_order = \WC_Helper_Order::create_order();
$test_order->set_payment_method( 'paypal' );
- $test_order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_COMPLETED );
+ $test_order->update_meta_data( '_paypal_status', PayPalConstants::STATUS_COMPLETED );
$test_order->save();
$custom_id_data = array(
@@ -304,7 +325,7 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
*
* @return array
*/
- public function provider_get_action_url_scenarios() {
+ public function provider_get_action_url_scenarios(): array {
return array(
'valid_capture_link' => array(
'links' => array(
@@ -377,10 +398,11 @@ class WC_Gateway_Paypal_Webhook_Handler_Test extends \WC_Unit_Test_Case {
* @param array $links The links array.
* @param string $action The action to find.
* @param mixed $expected The expected result.
+ * @return void
*/
- public function test_get_action_url_scenarios( $links, $action, $expected ) {
+ public function test_get_action_url_scenarios( array $links, string $action, $expected ): void {
// Use reflection to test private method.
- $reflection = new ReflectionClass( $this->webhook_handler );
+ $reflection = new \ReflectionClass( $this->webhook_handler );
$method = $reflection->getMethod( 'get_action_url' );
$method->setAccessible( true );