Commit 3f1bbd6cb1 for woocommerce

commit 3f1bbd6cb124a51bf9ffe84bca6b1356cb0d9888
Author: Wesley Rosa <wesleyjrosa@gmail.com>
Date:   Fri Dec 26 10:34:14 2025 -0300

    Handling auth already captured errors on PayPal Standard (#62582)

    * Handling auth already captured errors on PayPal Standard

    * Removint test code

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Moving the new constants

    * Unit test

    * Fix lint issues

    ---------

    Co-authored-by: github-actions <github-actions@github.com>

diff --git a/plugins/woocommerce/changelog/62582-fix-handling-auth-already-captured-errors-for-paypal b/plugins/woocommerce/changelog/62582-fix-handling-auth-already-captured-errors-for-paypal
new file mode 100644
index 0000000000..e850d72640
--- /dev/null
+++ b/plugins/woocommerce/changelog/62582-fix-handling-auth-already-captured-errors-for-paypal
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Handles authorization already captured errors on PayPal Standard by ignoring the error and properly setting the PayPal order status meta to `CAPTURED`.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-notices.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-notices.php
index dbc49c290e..d0612fab76 100644
--- a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-notices.php
+++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-notices.php
@@ -7,6 +7,8 @@

 declare(strict_types=1);

+use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
+
 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
 }
@@ -50,8 +52,8 @@ class WC_Gateway_Paypal_Notices {
 	 * @var array
 	 */
 	protected const PAYPAL_ACCOUNT_RESTRICTION_ISSUES = array(
-		'PAYEE_ACCOUNT_LOCKED_OR_CLOSED',
-		'PAYEE_ACCOUNT_RESTRICTED',
+		PayPalConstants::PAYPAL_ISSUE_PAYEE_ACCOUNT_LOCKED_OR_CLOSED,
+		PayPalConstants::PAYPAL_ISSUE_PAYEE_ACCOUNT_RESTRICTED,
 	);

 	/**
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 f925928c7a..37223ffe70 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
@@ -8,6 +8,7 @@
 declare(strict_types=1);

 use Automattic\WooCommerce\Gateways\PayPal\AddressRequirements as PayPalAddressRequirements;
+use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
 use Automattic\WooCommerce\Utilities\NumberUtil;
 use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client;
@@ -406,17 +407,19 @@ class WC_Gateway_Paypal_Request {
 				throw new Exception( 'PayPal capture payment request failed. Response error: ' . $response->get_error_message() );
 			}

-			$http_code     = wp_remote_retrieve_response_code( $response );
-			$body          = wp_remote_retrieve_body( $response );
-			$response_data = json_decode( $body, true );
+			$http_code             = wp_remote_retrieve_response_code( $response );
+			$body                  = wp_remote_retrieve_body( $response );
+			$response_data         = json_decode( $body, true );
+			$issue                 = isset( $response_data['details'][0]['issue'] ) ? $response_data['details'][0]['issue'] : '';
+			$auth_already_captured = 422 === $http_code && PayPalConstants::PAYPAL_ISSUE_AUTHORIZATION_ALREADY_CAPTURED === $issue;

-			if ( 200 !== $http_code && 201 !== $http_code ) {
+			if ( 200 !== $http_code && 201 !== $http_code && ! $auth_already_captured ) {
 				$paypal_debug_id = isset( $response_data['debug_id'] ) ? $response_data['debug_id'] : null;
 				throw new Exception( 'PayPal capture payment failed. Response status: ' . $http_code . '. Response body: ' . $body );
 			}

-			// Set custom status for successful capture response.
-			$order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_CAPTURED );
+			// Set custom status for successful capture response, or if the authorization was already captured.
+			$order->update_meta_data( '_paypal_status', PayPalConstants::STATUS_CAPTURED );
 			$order->save();
 		} catch ( Exception $e ) {
 			WC_Gateway_Paypal::log( $e->getMessage() );
diff --git a/plugins/woocommerce/src/Gateways/PayPal/Constants.php b/plugins/woocommerce/src/Gateways/PayPal/Constants.php
index e4a998c705..44f9c878c8 100644
--- a/plugins/woocommerce/src/Gateways/PayPal/Constants.php
+++ b/plugins/woocommerce/src/Gateways/PayPal/Constants.php
@@ -122,4 +122,25 @@ class Constants {
 		'USD', // United States Dollar.
 		'RUB', // Russian Ruble.
 	);
+
+	/**
+	 * PayPal authorization already captured issue code.
+	 *
+	 * @var string
+	 */
+	const PAYPAL_ISSUE_AUTHORIZATION_ALREADY_CAPTURED = 'AUTHORIZATION_ALREADY_CAPTURED';
+
+	/**
+	 * PayPal account locked or closed issue code.
+	 *
+	 * @var string
+	 */
+	const PAYPAL_ISSUE_PAYEE_ACCOUNT_LOCKED_OR_CLOSED = 'PAYEE_ACCOUNT_LOCKED_OR_CLOSED';
+
+	/**
+	 * PayPal account restricted issue code.
+	 *
+	 * @var string
+	 */
+	const PAYPAL_ISSUE_PAYEE_ACCOUNT_RESTRICTED = 'PAYEE_ACCOUNT_RESTRICTED';
 }
diff --git a/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php b/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php
index fb94fa55c3..f9d4f62adf 100644
--- a/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php
+++ b/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php
@@ -7,8 +7,9 @@

 declare(strict_types=1);

+use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
+
 require_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php';
-require_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-constants.php';

 /**
  * Class WC_Gateway_Paypal_Test.
@@ -398,8 +399,8 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 	 */
 	public function provider_already_captured_statuses() {
 		return array(
-			'status_captured'  => array( WC_Gateway_Paypal_Constants::STATUS_CAPTURED ),
-			'status_completed' => array( WC_Gateway_Paypal_Constants::STATUS_COMPLETED ),
+			'status_captured'  => array( PayPalConstants::STATUS_CAPTURED ),
+			'status_completed' => array( PayPalConstants::STATUS_COMPLETED ),
 		);
 	}

@@ -478,7 +479,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		// Verify status was updated.
 		$order = wc_get_order( $order->get_id() );
 		$this->assertEquals(
-			WC_Gateway_Paypal_Constants::STATUS_CAPTURED,
+			PayPalConstants::STATUS_CAPTURED,
 			$order->get_meta( '_paypal_status', true )
 		);
 	}
@@ -490,7 +491,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		$order = WC_Helper_Order::create_order();
 		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
 		$order->update_meta_data( '_paypal_authorization_id', 'AUTH_123' );
-		$order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED );
+		$order->update_meta_data( '_paypal_status', PayPalConstants::STATUS_AUTHORIZED );
 		$order->save();

 		$capture_api_call_count = 0;
@@ -532,7 +533,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		// Verify capture ID was not set.
 		$this->assertEmpty( $order->get_meta( '_paypal_capture_id', true ) );
 		// Verify status was not updated.
-		$this->assertEquals( WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED, $order->get_meta( '_paypal_status', true ) );
+		$this->assertEquals( PayPalConstants::STATUS_AUTHORIZED, $order->get_meta( '_paypal_status', true ) );
 	}

 	/**
@@ -542,7 +543,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		$order = WC_Helper_Order::create_order();
 		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
 		$order->update_meta_data( '_paypal_authorization_id', 'AUTH_123' );
-		$order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED );
+		$order->update_meta_data( '_paypal_status', PayPalConstants::STATUS_AUTHORIZED );
 		$order->save();

 		$capture_api_call_count = 0;
@@ -583,7 +584,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		// Verify capture ID was not set.
 		$this->assertEmpty( $order->get_meta( '_paypal_capture_id', true ) );
 		// Verify status was not updated.
-		$this->assertEquals( WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED, $order->get_meta( '_paypal_status', true ) );
+		$this->assertEquals( PayPalConstants::STATUS_AUTHORIZED, $order->get_meta( '_paypal_status', true ) );
 	}

 	/**
@@ -702,17 +703,17 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 											'authorizations' => array(
 												array(
 													'id' => 'AUTH_1',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-01-01T00:00:00Z',
 												),
 												array(
 													'id' => 'AUTH_3',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-02-02T00:00:00Z',
 												),
 												array(
 													'id' => 'AUTH_2',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-01-03T00:00:00Z',
 												),
 											),
@@ -771,19 +772,19 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 											'authorizations' => array(
 												array(
 													'id' => 'AUTH_1',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-01-01T00:00:00Z',
 												),
 											),
 											'captures' => array(
 												array(
 													'id' => 'CAPTURE_1',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_CAPTURED,
+													'status' => PayPalConstants::STATUS_CAPTURED,
 													'update_time' => '2024-01-01T00:00:00Z',
 												),
 												array(
 													'id' => 'CAPTURE_2',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_COMPLETED,
+													'status' => PayPalConstants::STATUS_COMPLETED,
 													'update_time' => '2024-02-02T00:00:00Z',
 												),
 											),
@@ -817,7 +818,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		// Verify capture ID was stored and no capture request was made.
 		$order = wc_get_order( $order->get_id() );
 		$this->assertEquals( 'CAPTURE_2', $order->get_meta( '_paypal_capture_id', true ) );
-		$this->assertEquals( WC_Gateway_Paypal_Constants::STATUS_COMPLETED, $order->get_meta( '_paypal_status', true ) );
+		$this->assertEquals( PayPalConstants::STATUS_COMPLETED, $order->get_meta( '_paypal_status', true ) );
 	}

 	/**
@@ -846,7 +847,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 											'authorizations' => array(
 												array(
 													'id' => 'AUTH_123',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_CAPTURED,
+													'status' => PayPalConstants::STATUS_CAPTURED,
 													'update_time' => '2024-01-01T00:00:00Z',
 												),
 											),
@@ -879,7 +880,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when authorization already captured' );
 		// Verify status was updated but no capture request was made.
 		$order = wc_get_order( $order->get_id() );
-		$this->assertEquals( WC_Gateway_Paypal_Constants::STATUS_CAPTURED, $order->get_meta( '_paypal_status', true ) );
+		$this->assertEquals( PayPalConstants::STATUS_CAPTURED, $order->get_meta( '_paypal_status', true ) );
 	}

 	/**
@@ -987,17 +988,17 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 											'authorizations' => array(
 												array(
 													'id' => 'AUTH_OLD',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-01-01T00:00:00Z',
 												),
 												array(
 													'id' => 'AUTH_NEWEST',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-01-03T00:00:00Z',
 												),
 												array(
 													'id' => 'AUTH_MIDDLE',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-01-02T00:00:00Z',
 												),
 											),
@@ -1149,12 +1150,12 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 											'authorizations' => array(
 												array(
 													'id' => 'AUTH_NO_TIME',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													// Missing update_time.
 												),
 												array(
 													'id' => 'AUTH_VALID',
-													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'status' => PayPalConstants::STATUS_AUTHORIZED,
 													'update_time' => '2024-01-01T00:00:00Z',
 												),
 											),
@@ -1232,6 +1233,62 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 		$this->assertEquals( 'yes', $order->get_meta( '_paypal_authorization_checked', true ) );
 	}

+	/**
+	 * Test capture handles already captured authorization errors (from the PayPal side).
+	 */
+	public function test_capture_authorized_payment_handles_auth_already_captured_errors() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->update_meta_data( '_paypal_authorization_id', 'AUTH_123' );
+		$order->save();
+
+		$capture_api_call_count = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$capture_api_call_count ) {
+				// Track if capture_auth endpoint is called.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					return array(
+						'response' => array( 'code' => 422 ),
+						'body'     => wp_json_encode(
+							array(
+								'name'     => 'UNPROCESSABLE_ENTITY',
+								'message'  => 'The requested action could not be performed, semantically incorrect, or failed business validation.',
+								'debug_id' => '1234567890',
+								'details'  => array(
+									array(
+										'issue'       => PayPalConstants::PAYPAL_ISSUE_AUTHORIZATION_ALREADY_CAPTURED,
+										'description' => 'The authorization has already been captured.',
+									),
+								),
+							)
+						),
+					);
+				}
+
+				return $value;
+			},
+			10,
+			3
+		);
+
+		$request = new WC_Gateway_Paypal_Request( new WC_Gateway_Paypal() );
+		$request->capture_authorized_payment( $order );
+
+		remove_all_filters( 'pre_http_request' );
+
+		// Verify capture_auth API was called once.
+		$this->assertEquals( 1, $capture_api_call_count, 'Expected capture_auth API to be called once' );
+
+		// Verify status was updated.
+		$order = wc_get_order( $order->get_id() );
+		$this->assertEquals(
+			PayPalConstants::STATUS_CAPTURED,
+			$order->get_meta( '_paypal_status', true )
+		);
+	}
+
 	// ========================================================================
 	// Helper methods for capture_authorized_payment tests
 	// ========================================================================
@@ -1250,7 +1307,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 			'body'     => wp_json_encode(
 				array(
 					'id'     => 'CAPTURE_123',
-					'status' => WC_Gateway_Paypal_Constants::STATUS_COMPLETED,
+					'status' => PayPalConstants::STATUS_COMPLETED,
 				)
 			),
 		);