Commit b1a51de370a for woocommerce

commit b1a51de370a8ed6e9c2aa07b94fb80be670a4570
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Thu Aug 6 15:44:02 2026 +0100

    Fix order withdrawal matching fields (#67433)

    * Fix order withdrawal matching fields

    * Update tests

    * test

diff --git a/plugins/woocommerce/changelog/fix-order-withdrawal-match-email-order-number b/plugins/woocommerce/changelog/fix-order-withdrawal-match-email-order-number
new file mode 100644
index 00000000000..f8f487bebba
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-order-withdrawal-match-email-order-number
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Match order withdrawal requests by email address and order number
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
index 173f5bc9301..f8d6a61fb89 100644
--- a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
@@ -364,7 +364,7 @@ final class OrderWithdrawalFormProcessor {
 	}

 	/**
-	 * Get an order only when every submitted order-identifying field matches.
+	 * Get an order only when the submitted email and order number match.
 	 *
 	 * @param array<string,string> $data Form data.
 	 */
@@ -417,15 +417,13 @@ final class OrderWithdrawalFormProcessor {
 	}

 	/**
-	 * Whether a candidate order exactly matches the submitted identifying data.
+	 * Whether a candidate order matches the submitted email and order number.
 	 *
 	 * @param WC_Order             $order Candidate order.
 	 * @param array<string,string> $data  Form data.
 	 */
 	private function order_matches_form_data( WC_Order $order, array $data ): bool {
 		return $this->normalize_order_number( (string) $order->get_order_number() ) === $this->normalize_order_number( $data[ self::FIELD_ORDER_NUMBER ] )
-			&& $this->text_values_match( $order->get_billing_first_name( 'edit' ), $data[ self::FIELD_FIRST_NAME ] )
-			&& $this->text_values_match( $order->get_billing_last_name( 'edit' ), $data[ self::FIELD_LAST_NAME ] )
 			&& $this->text_values_match( $order->get_billing_email( 'edit' ), $data[ self::FIELD_EMAIL ] );
 	}

diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
index 5f5d6aaed01..abc7f02af99 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
@@ -305,24 +305,21 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox Should not match an order that shares the email but has a different billing name.
+	 * @testdox Should match an order by the submitted order number when multiple orders share the email.
 	 */
-	public function test_process_current_request_matches_only_order_with_same_email_and_billing_name(): void {
-		$target_order         = $this->create_order_for_form_data();
-		$different_name_order = $this->create_order_for_form_data(
-			array(
-				OrderWithdrawalFormProcessor::FIELD_FIRST_NAME => 'Janet',
-				OrderWithdrawalFormProcessor::FIELD_LAST_NAME  => 'Smith',
-			)
-		);
-		$custom_order_number  = 'CUSTOM-1001';
-		$capture              = $this->capture_wp_mail();
-		$filter               = static function ( $order_number, $filtered_order ) use ( $target_order, $different_name_order, $custom_order_number ) {
-			if (
-				$filtered_order instanceof WC_Order
-				&& in_array( $filtered_order->get_id(), array( $target_order->get_id(), $different_name_order->get_id() ), true )
-			) {
-				return $custom_order_number;
+	public function test_process_current_request_matches_same_email_order_by_order_number(): void {
+		$target_order        = $this->create_order_for_form_data();
+		$different_order     = $this->create_order_for_form_data();
+		$target_order_number = 'CUSTOM-1001';
+		$other_order_number  = 'CUSTOM-2002';
+		$capture             = $this->capture_wp_mail();
+		$filter              = static function ( $order_number, $filtered_order ) use ( $target_order, $different_order, $target_order_number, $other_order_number ) {
+			if ( $filtered_order instanceof WC_Order && $target_order->get_id() === $filtered_order->get_id() ) {
+				return $target_order_number;
+			}
+
+			if ( $filtered_order instanceof WC_Order && $different_order->get_id() === $filtered_order->get_id() ) {
+				return $other_order_number;
 			}

 			return $order_number;
@@ -333,7 +330,7 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		try {
 			$this->prepare_post_request(
 				OrderWithdrawalFormProcessor::ACTION_CONFIRM,
-				array( OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER => $custom_order_number )
+				array( OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER => $target_order_number )
 			);

 			$state          = $this->sut->process_current_request();
@@ -342,16 +339,48 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 			$this->assertSame( 'confirmation', $state->screen, 'Matching submissions should reach the confirmation screen.' );
 			$this->assertCount( 2, $capture['captures'], 'The customer and merchant emails should both be sent.' );
 			$this->assertStringContainsString( str_replace( '&', '&amp;', $target_order->get_edit_order_url() ), (string) $merchant_email['message'], 'The merchant email should link to the intended order.' );
-			$this->assertStringNotContainsString( str_replace( '&', '&amp;', $different_name_order->get_edit_order_url() ), (string) $merchant_email['message'], 'The merchant email should not link to the order with a different billing name.' );
+			$this->assertStringNotContainsString( str_replace( '&', '&amp;', $different_order->get_edit_order_url() ), (string) $merchant_email['message'], 'The merchant email should not link to the order with a different order number.' );
 			$this->assertTrue( $this->order_has_note_containing( $target_order, self::ORDER_NOTE_WITHDRAWAL_REQUESTED ), 'The intended order should receive a withdrawal note.' );
-			$this->assertFalse( $this->order_has_note_containing( $different_name_order, 'Order withdrawal requested' ), 'The order with a different billing name should not receive a withdrawal note.' );
+			$this->assertFalse( $this->order_has_note_containing( $different_order, 'Order withdrawal requested' ), 'The order with a different order number should not receive a withdrawal note.' );
 			$this->assert_order_withdrawal_requested( $target_order );
+			$this->assert_order_withdrawal_not_requested( $different_order );
 		} finally {
 			remove_filter( 'woocommerce_order_number', $filter, 10 );
 			$capture['remove']();
 		}
 	}

+	/**
+	 * @testdox Should match an order with the correct email and order number even when the billing name differs.
+	 */
+	public function test_process_current_request_matches_order_with_different_billing_name(): void {
+		$order   = $this->create_order_for_form_data(
+			array(
+				OrderWithdrawalFormProcessor::FIELD_FIRST_NAME => 'Janet',
+				OrderWithdrawalFormProcessor::FIELD_LAST_NAME  => 'Smith',
+			)
+		);
+		$capture = $this->capture_wp_mail();
+
+		try {
+			$this->prepare_post_request(
+				OrderWithdrawalFormProcessor::ACTION_CONFIRM,
+				array( OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER => (string) $order->get_id() )
+			);
+
+			$state          = $this->sut->process_current_request();
+			$merchant_email = $this->get_captured_mail_to( (string) get_option( 'admin_email' ), $capture['captures'] );
+
+			$this->assertSame( 'confirmation', $state->screen, 'Matching submissions should reach the confirmation screen.' );
+			$this->assertCount( 2, $capture['captures'], 'The customer and merchant emails should both be sent.' );
+			$this->assertStringContainsString( str_replace( '&', '&amp;', $order->get_edit_order_url() ), (string) $merchant_email['message'], 'The merchant email should link to the matched order.' );
+			$this->assertTrue( $this->order_has_note_containing( $order, self::ORDER_NOTE_WITHDRAWAL_REQUESTED ), 'The order should receive a withdrawal note even when the billing name differs.' );
+			$this->assert_order_withdrawal_requested( $order );
+		} finally {
+			$capture['remove']();
+		}
+	}
+
 	/**
 	 * @testdox Should reject duplicate withdrawal requests for an already flagged matched order.
 	 */
@@ -1056,6 +1085,18 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		$this->assertSame( 'yes', $updated_order->get_meta( self::ORDER_WITHDRAWAL_REQUESTED_META_KEY, true, 'edit' ), 'The matched order should be flagged as having a withdrawal request.' );
 	}

+	/**
+	 * Assert that an order has not been flagged as having a withdrawal request.
+	 *
+	 * @param WC_Order $order Order.
+	 */
+	private function assert_order_withdrawal_not_requested( WC_Order $order ): void {
+		$updated_order = wc_get_order( $order->get_id() );
+
+		$this->assertInstanceOf( WC_Order::class, $updated_order, 'The order should still exist.' );
+		$this->assertNotSame( 'yes', $updated_order->get_meta( self::ORDER_WITHDRAWAL_REQUESTED_META_KEY, true, 'edit' ), 'The unmatched order should not be flagged as having a withdrawal request.' );
+	}
+
 	/**
 	 * Get the IDs of order withdrawal inbox notes created during a test.
 	 *