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