Commit a6d6866cd4 for woocommerce

commit a6d6866cd4608e374c957a5901ff819e0b344868
Author: Mayisha <33387139+Mayisha@users.noreply.github.com>
Date:   Tue Dec 2 23:36:14 2025 +0600

    PayPal Standard: Make auth capture logic more robust (#61963)

    * store capture and authorization ids in separate order meta

    * add AUTHORIZED status

    * check status and auth id from order meta

    * send paypal order id in auth capture request

    * refactor bailing condition

    * add order check

    * bail if the order is not transitioning from on-hold status

    * rearrange the bailing logic

    * retrieve auth id from paypal order

    * add status check

    * backward compatibility

    * revert order status check

    * skip if capture id is present

    * return if no paypal order id

    * simplify check with renamed function

    * save capture data

    * fix check and add meta

    * wrap in try catch

    * bail if capture id is present

    * guard against repeated call

    * get latest item from array

    * fix lint in constant file

    * fix lint

    * use dedicated flag meta

    * add tests

    * switch condition order

    * add log when capture/auth ID backfilled

    * string comparison

    * remove unnecessary empty value assignment

diff --git a/plugins/woocommerce/changelog/61963-task-update-auth-capture-logic b/plugins/woocommerce/changelog/61963-task-update-auth-capture-logic
new file mode 100644
index 0000000000..cd5ada79e9
--- /dev/null
+++ b/plugins/woocommerce/changelog/61963-task-update-auth-capture-logic
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Ensure PayPal Standard capture later flow uses the correct authorization ID.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php b/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php
index 479a3e1562..f46f6e907c 100644
--- a/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php
+++ b/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php
@@ -719,6 +719,9 @@ class WC_Gateway_Paypal extends WC_Payment_Gateway {
 	 */
 	public function capture_payment( $order_id ) {
 		$order = wc_get_order( $order_id );
+		if ( ! $order ) {
+			return;
+		}

 		// Bail if the order is not a PayPal order.
 		if ( self::ID !== $order->get_payment_method() ) {
diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-constants.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-constants.php
index 0abfbdf7d5..931af7b86a 100644
--- a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-constants.php
+++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-constants.php
@@ -26,9 +26,10 @@ class WC_Gateway_Paypal_Constants {
 	/**
 	 * PayPal payment statuses.
 	 */
-	const STATUS_COMPLETED = 'COMPLETED';
-	const STATUS_APPROVED  = 'APPROVED';
-	const STATUS_CAPTURED  = 'CAPTURED';
+	const STATUS_COMPLETED  = 'COMPLETED';
+	const STATUS_APPROVED   = 'APPROVED';
+	const STATUS_CAPTURED   = 'CAPTURED';
+	const STATUS_AUTHORIZED = 'AUTHORIZED';

 	/**
 	 * PayPal payment intents.
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 f5bd386256..12d2be08cc 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
@@ -322,24 +322,45 @@ class WC_Gateway_Paypal_Request {
 	 * @throws Exception If the PayPal payment capture fails.
 	 */
 	public function capture_authorized_payment( $order ) {
-		if ( ! $order || ! $order->get_transaction_id() ) {
-			WC_Gateway_Paypal::log( 'PayPal authorization ID not found. Cannot capture payment.' );
+		if ( ! $order ) {
+			WC_Gateway_Paypal::log( 'Order not found to capture authorized payment.' );
 			return;
 		}

+		$paypal_order_id = $order->get_meta( '_paypal_order_id', true );
+		// Skip if the PayPal Order ID is not found. This means the order was not created via the Orders v2 API.
+		if ( ! $paypal_order_id ) {
+			WC_Gateway_Paypal::log( 'PayPal Order ID not found to capture authorized payment. Order ID: ' . $order->get_id() );
+			return;
+		}
+
+		$capture_id = $order->get_meta( '_paypal_capture_id', true );
 		// Skip if the payment is already captured.
+		if ( $capture_id ) {
+			WC_Gateway_Paypal::log( 'PayPal payment is already captured. PayPal capture ID: ' . $capture_id . '. Order ID: ' . $order->get_id() );
+			return;
+		}
+
 		$paypal_status = $order->get_meta( '_paypal_status', true );
+		// Skip if the payment is already captured.
 		if ( WC_Gateway_Paypal_Constants::STATUS_CAPTURED === $paypal_status || WC_Gateway_Paypal_Constants::STATUS_COMPLETED === $paypal_status ) {
 			WC_Gateway_Paypal::log( 'PayPal payment is already captured. Skipping capture. Order ID: ' . $order->get_id() );
 			return;
 		}

+		$authorization_id = $this->get_authorization_id_for_capture( $order );
+		if ( ! $authorization_id ) {
+			WC_Gateway_Paypal::log( 'Authorization ID not found to capture authorized payment. Order ID: ' . $order->get_id() );
+			return;
+		}
+
 		$paypal_debug_id = null;

 		try {
 			$request_body = array(
 				'test_mode'        => $this->gateway->testmode,
-				'authorization_id' => $order->get_transaction_id(),
+				'authorization_id' => $authorization_id,
+				'paypal_order_id'  => $paypal_order_id,
 			);
 			$response     = $this->send_wpcom_proxy_request( 'POST', self::WPCOM_PROXY_PAYMENT_CAPTURE_AUTH_ENDPOINT, $request_body );

@@ -375,6 +396,108 @@ class WC_Gateway_Paypal_Request {
 		}
 	}

+	/**
+	 * Get the authorization ID for the PayPal payment.
+	 *
+	 * @param WC_Order $order Order object.
+	 * @return string|null
+	 */
+	private function get_authorization_id_for_capture( $order ) {
+		$paypal_order_id  = $order->get_meta( '_paypal_order_id', true );
+		$authorization_id = $order->get_meta( '_paypal_authorization_id', true );
+		$capture_id       = $order->get_meta( '_paypal_capture_id', true );
+
+		// If the PayPal order ID is not found or the capture ID is already set, return null.
+		if ( ! $paypal_order_id || ! empty( $capture_id ) ) {
+			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 ) ) {
+			return null;
+		}
+
+		// If the authorization ID is not found, try to retrieve it from the PayPal order details.
+		if ( empty( $authorization_id ) ) {
+			WC_Gateway_Paypal::log( 'Authorization ID not found, trying to retrieve from PayPal order details as a fallback for backwards compatibility. Order ID: ' . $order->get_id() );
+
+			try {
+				$order_data         = $this->get_paypal_order_details( $paypal_order_id );
+				$authorization_data = $this->get_latest_transaction_data(
+					$order_data['purchase_units'][0]['payments']['authorizations'] ?? array()
+				);
+
+				$capture_data = $this->get_latest_transaction_data(
+					$order_data['purchase_units'][0]['payments']['captures'] ?? array()
+				);
+
+				// If the payment is already captured, store the capture ID and status, and return null as there is no authorization ID that needs to be captured.
+				if ( $capture_data && isset( $capture_data['id'] ) ) {
+					$capture_id = $capture_data['id'];
+					$order->update_meta_data( '_paypal_capture_id', $capture_id );
+					$order->update_meta_data( '_paypal_status', $capture_data['status'] ?? WC_Gateway_Paypal_Constants::STATUS_CAPTURED );
+					$order->save();
+					WC_Gateway_Paypal::log( 'Storing capture ID from Paypal. Order ID: ' . $order->get_id() . '; capture ID: ' . $capture_id );
+					return null;
+				}
+
+				if ( $authorization_data && isset( $authorization_data['id'], $authorization_data['status'] ) ) {
+					// If the payment is already captured, return null as there is no authorization ID that needs to be captured.
+					if ( WC_Gateway_Paypal_Constants::STATUS_CAPTURED === $authorization_data['status'] ) {
+						$order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_CAPTURED );
+						$order->save();
+						return null;
+					}
+					$authorization_id = $authorization_data['id'];
+					$order->update_meta_data( '_paypal_authorization_id', $authorization_id );
+					$order->update_meta_data( '_paypal_status', WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED );
+					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.
+					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();
+					return null;
+				}
+			} catch ( Exception $e ) {
+				WC_Gateway_Paypal::log( 'Error retrieving authorization ID from PayPal order details. Order ID: ' . $order->get_id() . '. Error: ' . $e->getMessage() );
+				return null;
+			}
+		}
+
+		return $authorization_id;
+	}
+
+	/**
+	 * Get the latest item from the authorizations or captures array based on update_time.
+	 *
+	 * @param array $items Array of authorizations or captures.
+	 * @return array|null The latest authorization or capture or null if array is empty or no valid update_time found.
+	 */
+	private function get_latest_transaction_data( $items ) {
+		if ( empty( $items ) || ! is_array( $items ) ) {
+			return null;
+		}
+
+		$latest_item = null;
+		$latest_time = null;
+
+		foreach ( $items as $item ) {
+			if ( empty( $item['update_time'] ) ) {
+				continue;
+			}
+
+			if ( null === $latest_time || $item['update_time'] > $latest_time ) {
+				$latest_time = $item['update_time'];
+				$latest_item = $item;
+			}
+		}
+
+		return $latest_item;
+	}
+
 	/**
 	 * Get the approve link from the response data.
 	 *
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 8e317668ba..43d2944c90 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
@@ -132,6 +132,7 @@ class WC_Gateway_Paypal_Webhook_Handler {
 		$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(
@@ -166,6 +167,7 @@ class WC_Gateway_Paypal_Webhook_Handler {
 		$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 ) );
@@ -192,6 +194,8 @@ class WC_Gateway_Paypal_Webhook_Handler {

 		$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 */
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 c678a7ef9c..6110a2e3c1 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
@@ -8,6 +8,7 @@
 declare(strict_types=1);

 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.
@@ -33,6 +34,7 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
 	public function tearDown(): void {
 		remove_filter( 'pre_option_jetpack_options', array( $this, 'return_valid_site_id' ) );
 		remove_filter( 'pre_option_jetpack_private_options', array( $this, 'return_blog_token' ) );
+		remove_all_filters( 'pre_http_request' );

 		parent::tearDown();
 	}
@@ -288,4 +290,937 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {

 		$this->assertEquals( $expected, $result );
 	}
+
+	// ========================================================================
+	// Tests for capture_authorized_payment method
+	// ========================================================================
+
+	/**
+	 * Test capture is not attempted when order is null.
+	 */
+	public function test_capture_authorized_payment_not_attempted_when_order_is_null() {
+		$capture_api_call_count = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$capture_api_call_count ) {
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+				return $value;
+			},
+			10,
+			3
+		);
+
+		$request = new WC_Gateway_Paypal_Request( new WC_Gateway_Paypal() );
+		$request->capture_authorized_payment( null );
+
+		remove_all_filters( 'pre_http_request' );
+
+		// Verify capture_auth API was not called.
+		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when order is null' );
+	}
+
+	/**
+	 * Test capture is not attempted when PayPal Order ID is missing.
+	 */
+	public function test_capture_authorized_payment_not_attempted_when_paypal_order_id_missing() {
+		$order = WC_Helper_Order::create_order();
+		$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 $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 not called.
+		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when PayPal Order ID is missing' );
+	}
+
+	/**
+	 * Test capture is skipped when payment is already captured (via capture_id).
+	 */
+	public function test_capture_authorized_payment_skipped_when_already_captured_via_capture_id() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->update_meta_data( '_paypal_capture_id', 'CAPTURE_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 $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 not called.
+		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when payment already captured' );
+		// Verify status was not changed.
+		$this->assertEquals( 'CAPTURE_123', $order->get_meta( '_paypal_capture_id', true ) );
+	}
+
+	/**
+	 * Data provider for already captured status scenarios.
+	 *
+	 * @return array
+	 */
+	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 ),
+		);
+	}
+
+	/**
+	 * Test capture is skipped when payment status is already captured or completed.
+	 *
+	 * @dataProvider provider_already_captured_statuses
+	 *
+	 * @param string $status The payment status.
+	 */
+	public function test_capture_authorized_payment_skipped_when_status_already_captured( $status ) {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->update_meta_data( '_paypal_status', $status );
+		$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 $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 not called.
+		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when status is ' . $status );
+		// Verify status remained the same.
+		$this->assertEquals( $status, $order->get_meta( '_paypal_status', true ) );
+	}
+
+	/**
+	 * Test capture succeeds with HTTP 200 response.
+	 */
+	public function test_capture_authorized_payment_succeeds_with_http_200() {
+		$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 $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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(
+			WC_Gateway_Paypal_Constants::STATUS_CAPTURED,
+			$order->get_meta( '_paypal_status', true )
+		);
+	}
+
+	/**
+	 * Test capture fails with various HTTP error codes.
+	 */
+	public function test_capture_authorized_payment_fails() {
+		$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;
+		$debug_id               = 'DEBUG_ID_12345';
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$capture_api_call_count, $debug_id ) {
+				// Track if capture_auth endpoint is called.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					return $this->return_capture_error( 400, array( 'debug_id' => $debug_id ) );
+				}
+
+				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 (but returned error).
+		$this->assertEquals( 1, $capture_api_call_count, 'Expected capture_auth API to be called once' );
+		// Verify order note was added.
+		$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( $debug_id, $notes[0]->content );
+		// 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.
+	 */
+	public function test_capture_authorized_payment_handles_wp_error_response() {
+		$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 new WP_Error( 'http_request_failed', 'Connection timeout' );
+				}
+
+				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 (but returned WP_Error).
+		$this->assertEquals( 1, $capture_api_call_count, 'Expected capture_auth API to be called once' );
+		// Verify order note was added.
+		$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 );
+	}
+
+	/**
+	 * Test capture request includes correct parameters.
+	 */
+	public function test_capture_authorized_payment_request_includes_correct_parameters() {
+		$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();
+
+		$captured_request       = null;
+		$capture_api_call_count = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$captured_request, &$capture_api_call_count ) {
+				// Capture the capture_auth request.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					$captured_request = $parsed_args;
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 exactly once.
+		$this->assertEquals( 1, $capture_api_call_count, 'Expected capture_auth API to be called once' );
+		$this->assertNotNull( $captured_request, 'Expected to capture the request' );
+		$this->assertEquals( 'POST', $captured_request['method'] );
+		$this->assertEquals( 'application/json', $captured_request['headers']['Content-Type'] );
+
+		$body = json_decode( $captured_request['body'], true );
+		$this->assertIsArray( $body );
+		$this->assertEquals( 'AUTH_123', $body['authorization_id'] );
+		$this->assertEquals( 'PAYPAL_ORDER_123', $body['paypal_order_id'] );
+		$this->assertArrayHasKey( 'test_mode', $body );
+	}
+
+	/**
+	 * Test authorization ID is retrieved from API when not in meta.
+	 */
+	public function test_capture_authorized_payment_retrieves_authorization_id_from_api() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		// Don't set _paypal_authorization_id.
+		$order->save();
+
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) {
+				// Mock get PayPal order details API call.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 200 ),
+						'body'     => wp_json_encode(
+							array(
+								'id'             => 'PAYPAL_ORDER_123',
+								'status'         => 'COMPLETED',
+								'purchase_units' => array(
+									array(
+										'payments' => array(
+											'authorizations' => array(
+												array(
+													'id' => 'AUTH_1',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-01-01T00:00:00Z',
+												),
+												array(
+													'id' => 'AUTH_3',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-02-02T00:00:00Z',
+												),
+												array(
+													'id' => 'AUTH_2',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-01-03T00:00:00Z',
+												),
+											),
+										),
+									),
+								),
+							)
+						),
+					);
+				}
+
+				// Mock capture API call.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 authorization ID was stored.
+		$order = wc_get_order( $order->get_id() );
+		$this->assertEquals( 'AUTH_3', $order->get_meta( '_paypal_authorization_id', true ) );
+	}
+
+	/**
+	 * Test capture is skipped when API returns capture data in order details.
+	 */
+	public function test_capture_authorized_payment_skipped_when_api_returns_capture_data() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->save();
+
+		$capture_api_call_count = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$capture_api_call_count ) {
+				// Mock get PayPal order details API call.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 200 ),
+						'body'     => wp_json_encode(
+							array(
+								'id'             => 'PAYPAL_ORDER_123',
+								'status'         => 'COMPLETED',
+								'purchase_units' => array(
+									array(
+										'payments' => array(
+											'authorizations' => array(
+												array(
+													'id' => 'AUTH_1',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-01-01T00:00:00Z',
+												),
+											),
+											'captures' => array(
+												array(
+													'id' => 'CAPTURE_1',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_CAPTURED,
+													'update_time' => '2024-01-01T00:00:00Z',
+												),
+												array(
+													'id' => 'CAPTURE_2',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_COMPLETED,
+													'update_time' => '2024-02-02T00:00:00Z',
+												),
+											),
+										),
+									),
+								),
+							)
+						),
+					);
+				}
+
+				// Track if capture_auth endpoint is called.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 NOT called.
+		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when capture already exists' );
+		// 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 ) );
+	}
+
+	/**
+	 * Test capture is skipped when authorization status is already CAPTURED.
+	 */
+	public function test_capture_authorized_payment_skipped_when_authorization_status_is_captured() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->save();
+
+		$capture_api_call_count = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$capture_api_call_count ) {
+				// Mock get PayPal order details API call.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 200 ),
+						'body'     => wp_json_encode(
+							array(
+								'id'             => 'PAYPAL_ORDER_123',
+								'status'         => 'COMPLETED',
+								'purchase_units' => array(
+									array(
+										'payments' => array(
+											'authorizations' => array(
+												array(
+													'id' => 'AUTH_123',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_CAPTURED,
+													'update_time' => '2024-01-01T00:00:00Z',
+												),
+											),
+										),
+									),
+								),
+							)
+						),
+					);
+				}
+
+				// Track if capture_auth endpoint is called.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 NOT called.
+		$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 ) );
+	}
+
+	/**
+	 * Test authorization checked flag prevents repeated API calls.
+	 */
+	public function test_capture_authorized_payment_authorization_checked_flag_prevents_repeated_calls() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->update_meta_data( '_paypal_authorization_checked', 'yes' );
+		$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 $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 not called.
+		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when authorization_checked flag is set' );
+		// Verify capture ID was not set.
+		$this->assertEmpty( $order->get_meta( '_paypal_capture_id', true ) );
+	}
+
+	/**
+	 * Test capture handles API exception during authorization ID retrieval.
+	 */
+	public function test_capture_authorized_payment_handles_api_exception_during_retrieval() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->save();
+
+		$capture_api_call_count = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$capture_api_call_count ) {
+				// Mock get PayPal order details API call with error.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 500 ),
+						'body'     => wp_json_encode( array( 'error' => 'Internal Server Error' ) ),
+					);
+				}
+
+				// Track if capture_auth endpoint is called.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 NOT called after error.
+		$this->assertEquals( 0, $capture_api_call_count, 'Expected no capture_auth API call when order details retrieval fails' );
+		// Verify capture ID was not set.
+		$order = wc_get_order( $order->get_id() );
+		$this->assertEmpty( $order->get_meta( '_paypal_capture_id', true ) );
+	}
+
+	/**
+	 * Test get_latest_transaction_data selects most recent authorization.
+	 */
+	public function test_capture_authorized_payment_selects_most_recent_authorization() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->save();
+
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) {
+				// Mock get PayPal order details API call with multiple authorizations.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 200 ),
+						'body'     => wp_json_encode(
+							array(
+								'id'             => 'PAYPAL_ORDER_123',
+								'status'         => 'COMPLETED',
+								'purchase_units' => array(
+									array(
+										'payments' => array(
+											'authorizations' => array(
+												array(
+													'id' => 'AUTH_OLD',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-01-01T00:00:00Z',
+												),
+												array(
+													'id' => 'AUTH_NEWEST',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-01-03T00:00:00Z',
+												),
+												array(
+													'id' => 'AUTH_MIDDLE',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-01-02T00:00:00Z',
+												),
+											),
+										),
+									),
+								),
+							)
+						),
+					);
+				}
+
+				// Mock capture API call.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 the most recent authorization ID was stored.
+		$order = wc_get_order( $order->get_id() );
+		$this->assertEquals( 'AUTH_NEWEST', $order->get_meta( '_paypal_authorization_id', true ) );
+	}
+
+	/**
+	 * Test capture in test mode vs production mode.
+	 */
+	public function test_capture_authorized_payment_respects_test_mode_setting() {
+		$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();
+
+		// Test with test mode enabled.
+		$gateway           = new WC_Gateway_Paypal();
+		$gateway->testmode = true;
+
+		$captured_request       = null;
+		$capture_api_call_count = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) use ( &$captured_request, &$capture_api_call_count ) {
+				// Capture the capture_auth request.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					++$capture_api_call_count;
+					$captured_request = $parsed_args;
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				return $value;
+			},
+			10,
+			3
+		);
+
+		$request = new WC_Gateway_Paypal_Request( $gateway );
+		$request->capture_authorized_payment( $order );
+
+		remove_all_filters( 'pre_http_request' );
+
+		// Verify capture_auth API was called.
+		$this->assertEquals( 1, $capture_api_call_count, 'Expected capture_auth API to be called' );
+		$body = json_decode( $captured_request['body'], true );
+		$this->assertTrue( $body['test_mode'], 'Expected test_mode to be true' );
+	}
+
+	/**
+	 * Test capture handles empty authorization array from API.
+	 */
+	public function test_capture_authorized_payment_handles_empty_authorization_array() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->save();
+
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) {
+				// Mock get PayPal order details API call with empty authorizations.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 200 ),
+						'body'     => wp_json_encode(
+							array(
+								'id'             => 'PAYPAL_ORDER_123',
+								'status'         => 'COMPLETED',
+								'purchase_units' => array(
+									array(
+										'payments' => array(
+											'authorizations' => array(),
+										),
+									),
+								),
+							)
+						),
+					);
+				}
+
+				// Mock capture API call.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 authorization_checked flag was set.
+		$order = wc_get_order( $order->get_id() );
+		$this->assertEquals( 'yes', $order->get_meta( '_paypal_authorization_checked', true ) );
+	}
+
+	/**
+	 * Test capture handles authorization with invalid update_time.
+	 */
+	public function test_capture_authorized_payment_handles_invalid_update_time() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->save();
+
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) {
+				// Mock get PayPal order details API call with invalid update_time.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 200 ),
+						'body'     => wp_json_encode(
+							array(
+								'id'             => 'PAYPAL_ORDER_123',
+								'status'         => 'COMPLETED',
+								'purchase_units' => array(
+									array(
+										'payments' => array(
+											'authorizations' => array(
+												array(
+													'id' => 'AUTH_NO_TIME',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													// Missing update_time.
+												),
+												array(
+													'id' => 'AUTH_VALID',
+													'status' => WC_Gateway_Paypal_Constants::STATUS_AUTHORIZED,
+													'update_time' => '2024-01-01T00:00:00Z',
+												),
+											),
+										),
+									),
+								),
+							)
+						),
+					);
+				}
+
+				// Mock capture API call.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 the valid authorization was used.
+		$order = wc_get_order( $order->get_id() );
+		$this->assertEquals( 'AUTH_VALID', $order->get_meta( '_paypal_authorization_id', true ) );
+	}
+
+	/**
+	 * Test capture handles missing purchase_units in API response.
+	 */
+	public function test_capture_authorized_payment_handles_missing_purchase_units() {
+		$order = WC_Helper_Order::create_order();
+		$order->update_meta_data( '_paypal_order_id', 'PAYPAL_ORDER_123' );
+		$order->save();
+
+		add_filter(
+			'pre_http_request',
+			function ( $value, $parsed_args, $url ) {
+				// Mock get PayPal order details API call without purchase_units.
+				if ( strpos( $url, 'order/PAYPAL_ORDER_123' ) !== false ) {
+					return array(
+						'response' => array( 'code' => 200 ),
+						'body'     => wp_json_encode(
+							array(
+								'id'     => 'PAYPAL_ORDER_123',
+								'status' => 'COMPLETED',
+							)
+						),
+					);
+				}
+
+				// Mock capture API call.
+				if ( strpos( $url, 'payment/capture_auth' ) !== false ) {
+					return $this->return_capture_success_200( $value, $parsed_args );
+				}
+
+				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 authorization_checked flag was set.
+		$order = wc_get_order( $order->get_id() );
+		$this->assertEquals( 'yes', $order->get_meta( '_paypal_authorization_checked', true ) );
+	}
+
+	// ========================================================================
+	// Helper methods for capture_authorized_payment tests
+	// ========================================================================
+
+	/**
+	 * Helper method to return HTTP 200 success response for capture.
+	 *
+	 * @param mixed $value      Original value.
+	 * @param array $parsed_args Parsed arguments.
+	 *
+	 * @return array
+	 */
+	public function return_capture_success_200( $value, $parsed_args ) {
+		return array(
+			'response' => array( 'code' => 200 ),
+			'body'     => wp_json_encode(
+				array(
+					'id'     => 'CAPTURE_123',
+					'status' => WC_Gateway_Paypal_Constants::STATUS_COMPLETED,
+				)
+			),
+		);
+	}
+
+	/**
+	 * Helper method to return error response for capture.
+	 *
+	 * @param int   $http_code HTTP error code.
+	 * @param array $body_data Additional body data.
+	 *
+	 * @return array
+	 */
+	public function return_capture_error( $http_code, $body_data = array() ) {
+		$default_body = array(
+			'name'    => 'ERROR',
+			'message' => 'An error occurred',
+		);
+
+		return array(
+			'response' => array( 'code' => $http_code ),
+			'body'     => wp_json_encode( array_merge( $default_body, $body_data ) ),
+		);
+	}
 }