Commit fda858191a for woocommerce
commit fda858191a26d4d1399a53cad1dff4438930c596
Author: Mayisha <33387139+Mayisha@users.noreply.github.com>
Date: Mon Dec 8 20:33:20 2025 +0600
PayPal Standard: Add meta to prevent duplicate auth-capture request when resource not found (#62304)
* set flag when auth ID not found in paypal
* add note
* update check
* fix order note and save
* Add changefile(s) from automation for the following project(s): woocommerce
* add test
* declare variable
* fix lint
* update changelog
* update comment
* update note
* update test
* add escaping
diff --git a/plugins/woocommerce/changelog/62304-add-paypal-standard-additiona-meta-to-prevent-duplicate-requests b/plugins/woocommerce/changelog/62304-add-paypal-standard-additiona-meta-to-prevent-duplicate-requests
new file mode 100644
index 0000000000..e7c7ce54b6
--- /dev/null
+++ b/plugins/woocommerce/changelog/62304-add-paypal-standard-additiona-meta-to-prevent-duplicate-requests
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Add order meta to prevent duplicate PayPal Standard API calls when we have already received a 404 response for a capture request on an authorised payment.
\ 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 e64faec933..a6e2e94dbd 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
@@ -376,6 +376,7 @@ class WC_Gateway_Paypal_Request {
}
$paypal_debug_id = null;
+ $http_code = null;
try {
$request_body = array(
@@ -403,9 +404,30 @@ class WC_Gateway_Paypal_Request {
$order->save();
} catch ( Exception $e ) {
WC_Gateway_Paypal::log( $e->getMessage() );
+
$note_message = sprintf(
__( 'PayPal capture authorized payment failed', 'woocommerce' ),
);
+
+ // Scenario 1: Capture auth API call returned 404 (authorization object does not exist).
+ // If the authorization ID is not found (404 response), set the '_paypal_authorization_checked' flag.
+ // This flag indicates that we've made an API call to capture PayPal payment and no authorization object was found with this authorization ID.
+ // This prevents repeated API calls for orders that have no authorization data.
+ if ( 404 === $http_code ) {
+ $paypal_dashboard_url = $this->gateway->testmode
+ ? 'https://www.sandbox.paypal.com/unifiedtransactions'
+ : 'https://www.paypal.com/unifiedtransactions';
+
+ $note_message .= sprintf(
+ /* translators: %1$s: Authorization ID, %2$s: open link tag, %3$s: close link tag */
+ __( '. Authorization ID: %1$s not found. Please log into your %2$sPayPal account%3$s to capture the payment', 'woocommerce' ),
+ esc_html( $authorization_id ),
+ '<a href="' . esc_url( $paypal_dashboard_url ) . '" target="_blank">',
+ '</a>'
+ );
+ $order->update_meta_data( '_paypal_authorization_checked', 'yes' );
+ }
+
if ( $paypal_debug_id ) {
$note_message .= sprintf(
/* translators: %s: PayPal debug ID */
@@ -413,7 +435,9 @@ class WC_Gateway_Paypal_Request {
$paypal_debug_id
);
}
+
$order->add_order_note( $note_message );
+ $order->save();
}
}
@@ -433,9 +457,12 @@ class WC_Gateway_Paypal_Request {
return null;
}
- // If '_paypal_authorization_checked' is set to 'yes' and the authorization ID is empty, it means we already checked and found no authorization data.
- // Return null to avoid repeated API calls.
- if ( empty( $authorization_id ) && 'yes' === $order->get_meta( '_paypal_authorization_checked', true ) ) {
+ // If '_paypal_authorization_checked' is set to 'yes', it means we've already made an API call to PayPal
+ // and confirmed that no authorization object exists. This flag is set in two scenarios:
+ // 1. Capture auth API call returned 404 (authorization object does not exist with the authorization ID).
+ // 2. Order details API call returned empty authorization array (authorization object does not exist for this PayPal order).
+ // Return null to avoid repeated API calls for orders that have no authorization data.
+ if ( 'yes' === $order->get_meta( '_paypal_authorization_checked', true ) ) {
return null;
}
@@ -476,7 +503,9 @@ class WC_Gateway_Paypal_Request {
WC_Gateway_Paypal::log( 'Storing authorization ID from Paypal. Order ID: ' . $order->get_id() . '; authorization ID: ' . $authorization_id );
$order->save();
} else {
- // Store '_paypal_authorization_checked' flag to prevent repeated API calls for orders with no authorization data.
+ // Scenario 2: Order details API call returned empty authorization array (authorization object does not exist).
+ // Store '_paypal_authorization_checked' flag to prevent repeated API calls.
+ // This flag indicates that we've made an API call to get PayPal order details and confirmed no authorization object exists.
WC_Gateway_Paypal::log( 'Authorization ID not found in PayPal order details. Order ID: ' . $order->get_id() );
$order->update_meta_data( '_paypal_authorization_checked', 'yes' );
$order->save();
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 6110a2e3c1..6f5266b897 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
@@ -534,6 +534,57 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
$this->assertEquals( WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED, $order->get_meta( '_paypal_status', true ) );
}
+ /**
+ * Test capture handles 404 error and sets authorization_checked flag.
+ */
+ public function test_capture_authorized_payment_handles_404_error_and_sets_authorization_checked_flag() {
+ $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->save();
+
+ $capture_api_call_count = 0;
+ $authorization_id = 'AUTH_123';
+
+ $filter_callback = 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 $this->return_capture_error( 404, array() );
+ }
+
+ return $value;
+ };
+ add_filter( 'pre_http_request', $filter_callback, 10, 3 );
+
+ $request = new WC_Gateway_Paypal_Request( new WC_Gateway_Paypal() );
+ $request->capture_authorized_payment( $order );
+
+ remove_filter( 'pre_http_request', $filter_callback, 10 );
+
+ // Verify capture_auth API was called (but returned 404).
+ $this->assertEquals( 1, $capture_api_call_count, 'Expected capture_auth API to be called once' );
+ // Verify order note was added with authorization ID message.
+ $order = wc_get_order( $order->get_id() );
+ $notes = wc_get_order_notes(
+ array(
+ 'order_id' => $order->get_id(),
+ 'limit' => 1,
+ )
+ );
+
+ $this->assertNotEmpty( $notes );
+ $this->assertStringContainsString( 'PayPal capture authorized payment failed', $notes[0]->content );
+ $this->assertStringContainsString( 'Authorization ID: ' . $authorization_id . ' not found', $notes[0]->content );
+ // Verify authorization_checked flag was set.
+ $this->assertEquals( 'yes', $order->get_meta( '_paypal_authorization_checked', true ), 'Expected _paypal_authorization_checked flag to be set to yes' );
+ // 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 ) );
+ }
+
/**
* Test capture handles WP_Error response.
*/