Commit ba2023c0da4 for woocommerce

commit ba2023c0da4eac7835485a96c552df254df6c56d
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Tue Sep 22 14:18:22 2026 +0100

    Fix withdrawal request rate limit bypass (#68685)

    * Fix withdrawal request rate limit bypass

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Dont reset rate limit from failed submission

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/68685-fix-order-withdrawal-rate-limit-reset b/plugins/woocommerce/changelog/68685-fix-order-withdrawal-rate-limit-reset
new file mode 100644
index 00000000000..01f11d34caf
--- /dev/null
+++ b/plugins/woocommerce/changelog/68685-fix-order-withdrawal-rate-limit-reset
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep order withdrawal requests rate limited when an order already has a withdrawal request.
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
index 4e1feb693d4..45a192cba2e 100644
--- a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
@@ -288,14 +288,18 @@ final class OrderWithdrawalFormProcessor {
 				'error'
 			);

-			$this->apply_rate_limits( $rate_limit_ids, -1 );
-
 			return false;
 		}

 		if ( ! $this->send_order_withdrawal_emails( $data, $matched_order ) ) {
-			wc_add_notice( __( 'We could not submit your withdrawal request. Please try again or contact us if the problem continues.', 'woocommerce' ), 'error' );
-			$this->apply_rate_limits( $rate_limit_ids, -1 );
+			wc_add_notice(
+				sprintf(
+					/* translators: %d: number of seconds before another withdrawal request can be submitted. */
+					__( 'We could not submit your withdrawal request. Please try again in %d seconds or contact us if the problem continues.', 'woocommerce' ),
+					self::RATE_LIMIT_DELAY
+				),
+				'error'
+			);

 			return false;
 		}
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
index a3c7842728d..6176add9230 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
@@ -423,9 +423,9 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 			$second_state         = $this->sut->process_current_request();
 			$second_error_notices = wc_get_notices( 'error' );

-			$this->assertSame( 'review', $second_state->screen, 'Duplicate matched submissions should not leave behind a rate limit.' );
-			$this->assertCount( 1, $second_error_notices, 'The second duplicate submission should add only the duplicate-order error notice.' );
-			$this->assertStringContainsString( 'already been submitted for this order', $second_error_notices[0]['notice'], 'The released rate limit should allow duplicate-order validation to run again.' );
+			$this->assertSame( 'review', $second_state->screen, 'Duplicate matched submissions should remain on the review screen when rate limited.' );
+			$this->assertCount( 1, $second_error_notices, 'The second duplicate submission should add only the rate-limit error notice.' );
+			$this->assertStringContainsString( 'Please wait before submitting another withdrawal request.', $second_error_notices[0]['notice'], 'Duplicate matched submissions should remain rate limited.' );
 		} finally {
 			$capture['remove']();
 		}
@@ -835,7 +835,7 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 			$this->assertSame( 'review', $state->screen, 'Email failures should keep the submitted details on the review screen.' );
 			$this->assertCount( 2, $capture['captures'], 'The processor should attempt both notification emails before surfacing the failure.' );
 			$this->assertNotEmpty( $error_notices, 'Email failures should add an error notice.' );
-			$this->assertStringContainsString( 'We could not submit your withdrawal request.', $error_notices[0]['notice'], 'The error notice should tell the user the submission did not complete.' );
+			$this->assertStringContainsString( 'We could not submit your withdrawal request. Please try again in 30 seconds or contact us if the problem continues.', $error_notices[0]['notice'], 'The error notice should tell the user when they can retry the submission.' );
 			$this->assertFalse( $this->order_has_note_containing( $order, 'Order withdrawal requested' ), 'Email failures should not add a retryable request to the order notes.' );

 			$updated_order = wc_get_order( $order->get_id() );
@@ -857,10 +857,10 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 			$second_state         = $this->sut->process_current_request();
 			$second_error_notices = wc_get_notices( 'error' );

-			$this->assertSame( 'review', $second_state->screen, 'Email failures should not leave behind a rate limit.' );
-			$this->assertCount( 4, $capture['captures'], 'The second failed submission should attempt notification emails again.' );
-			$this->assertNotEmpty( $second_error_notices, 'The second email failure should add an error notice.' );
-			$this->assertStringContainsString( 'We could not submit your withdrawal request.', $second_error_notices[0]['notice'], 'The released rate limit should allow email delivery to be attempted again.' );
+			$this->assertSame( 'review', $second_state->screen, 'Rate-limited submissions should remain on the review screen.' );
+			$this->assertCount( 2, $capture['captures'], 'The rate limit should prevent another email delivery attempt.' );
+			$this->assertNotEmpty( $second_error_notices, 'The rate-limited submission should add an error notice.' );
+			$this->assertStringContainsString( 'Please wait before submitting another withdrawal request.', $second_error_notices[0]['notice'], 'The rate limit should prevent an immediate retry.' );
 		} finally {
 			$capture['remove']();
 		}