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.
 	 */