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