Commit c3270b5c736 for woocommerce

commit c3270b5c736d80295d3a06c8b05a33439b0ce104
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Tue Aug 4 14:48:58 2026 +0100

    Add withdrawal window warning for merchants (#67160)

    * Add withdrawal window warning for merchants

    * Add changelog for withdrawal window warning

    * Update warning message

    * Merge condition statementsfor messaging

    * Add regression test

    * Translator comments

    * Merge tests

    * Merge

    * Fix linting

diff --git a/plugins/woocommerce/changelog/add-withdraw-order-window-warning b/plugins/woocommerce/changelog/add-withdraw-order-window-warning
new file mode 100644
index 00000000000..84975598080
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-withdraw-order-window-warning
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add merchant warning for order withdrawal requests outside the 14-day window.
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
index 67956193205..173f5bc9301 100644
--- a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
@@ -40,6 +40,8 @@ final class OrderWithdrawalFormProcessor {
 	private const LOGGER_SOURCE                       = 'order-withdrawal';
 	private const ORDER_WITHDRAWAL_REQUESTED_META_KEY = '_order_withdrawal_requested';
 	private const ORDER_WITHDRAWAL_REQUESTED_VALUE    = 'yes';
+	private const WITHDRAWAL_WINDOW_IN_DAYS           = 14;
+	private const WITHDRAWAL_WINDOW_IN_SECONDS        = self::WITHDRAWAL_WINDOW_IN_DAYS * DAY_IN_SECONDS;
 	private const INBOX_NOTE_NAME_PREFIX              = 'wc-order-withdrawal-requested-order-';
 	private const RATE_LIMIT_IP_PREFIX                = 'order_withdrawal_ip_';
 	private const RATE_LIMIT_EMAIL_PREFIX             = 'order_withdrawal_email_';
@@ -481,6 +483,16 @@ final class OrderWithdrawalFormProcessor {
 	 */
 	private function add_order_withdrawal_inbox_note( WC_Order $matched_order ): void {
 		try {
+			$content = sprintf(
+				/* translators: %s: order number. */
+				__( 'A customer submitted an order withdrawal request for order #%s. Review the matched order to confirm the request details.', 'woocommerce' ),
+				$matched_order->get_order_number()
+			);
+
+			if ( $this->is_order_outside_withdrawal_window( $matched_order ) ) {
+				$content .= ' ' . $this->get_withdrawal_window_warning_message();
+			}
+
 			$note = new Note();
 			$note->set_title(
 				sprintf(
@@ -489,13 +501,7 @@ final class OrderWithdrawalFormProcessor {
 					$matched_order->get_order_number()
 				)
 			);
-			$note->set_content(
-				sprintf(
-				/* translators: %s: order number. */
-					__( 'A customer submitted an order withdrawal request for order #%s. Review the matched order to confirm the request details.', 'woocommerce' ),
-					$matched_order->get_order_number()
-				)
-			);
+			$note->set_content( $content );
 			$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
 			$note->set_name( self::INBOX_NOTE_NAME_PREFIX . $matched_order->get_id() );
 			$note->set_source( 'woocommerce-admin' );
@@ -541,6 +547,33 @@ final class OrderWithdrawalFormProcessor {
 		}
 	}

+	/**
+	 * Whether the matched order is outside the valid withdrawal request window.
+	 *
+	 * @param WC_Order $order Matched order.
+	 */
+	private function is_order_outside_withdrawal_window( WC_Order $order ): bool {
+		$date_created = $order->get_date_created( 'edit' );
+
+		if ( ! $date_created ) {
+			return false;
+		}
+
+		return ( time() - self::WITHDRAWAL_WINDOW_IN_SECONDS ) > $date_created->getTimestamp();
+	}
+
+	/**
+	 * Get the warning shown to merchants when a request is outside the valid window.
+	 */
+	private function get_withdrawal_window_warning_message(): string {
+		return sprintf(
+			/* translators: 1: number of days since the order was placed. 2: length of the withdrawal window in days. */
+			__( 'This order is older than %1$d days. Only orders within %2$d days of delivery are eligible for withdrawal.', 'woocommerce' ),
+			self::WITHDRAWAL_WINDOW_IN_DAYS,
+			self::WITHDRAWAL_WINDOW_IN_DAYS
+		);
+	}
+
 	/**
 	 * Whether the matched order already has a submitted withdrawal request.
 	 *
@@ -649,6 +682,10 @@ final class OrderWithdrawalFormProcessor {
 		if ( $matched_order instanceof WC_Order ) {
 			$order_url = $matched_order->get_edit_order_url();

+			if ( $this->is_order_outside_withdrawal_window( $matched_order ) ) {
+				$body .= '<p>' . esc_html( $this->get_withdrawal_window_warning_message() ) . '</p>';
+			}
+
 			$body .= sprintf(
 				'<p>%s</p>',
 				sprintf(
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
index 9d997df6389..af70f40538a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
@@ -500,6 +500,55 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		}
 	}

+	/**
+	 * @testdox Should warn merchants only when the matched order is older than fourteen days.
+	 * @testWith [15, true]
+	 *           [13, false]
+	 *
+	 * @param int  $order_age_days     Order age in days.
+	 * @param bool $is_warning_expected Whether the warning should be included for merchants.
+	 */
+	public function test_process_current_request_warns_merchants_only_for_orders_older_than_fourteen_days( int $order_age_days, bool $is_warning_expected ): void {
+		$order = $this->create_order_for_form_data();
+		$order->set_date_created( time() - ( $order_age_days * DAY_IN_SECONDS ) );
+		$order->save();
+
+		$capture         = $this->capture_wp_mail();
+		$warning_message = 'This order is older than 14 days. Only orders within 14 days of delivery are eligible for withdrawal.';
+
+		try {
+			$this->prepare_post_request(
+				OrderWithdrawalFormProcessor::ACTION_CONFIRM,
+				array( OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER => (string) $order->get_id() )
+			);
+
+			$state    = $this->sut->process_current_request();
+			$note_ids = $this->get_created_inbox_note_ids();
+
+			$this->assertSame( 'confirmation', $state->screen, 'Matched confirm submissions should reach the confirmation screen.' );
+			$this->assertCount( 1, $note_ids, 'A matched submission should create one merchant inbox notification.' );
+
+			$note = Notes::get_note( $note_ids[0] );
+
+			$this->assertInstanceOf( Note::class, $note, 'The merchant inbox notification should be readable.' );
+
+			$customer_email = $this->get_captured_mail_to( 'jane@example.test', $capture['captures'] );
+			$merchant_email = $this->get_captured_mail_to( (string) get_option( 'admin_email' ), $capture['captures'] );
+
+			if ( $is_warning_expected ) {
+				$this->assertStringContainsString( $warning_message, $note->get_content(), 'The inbox notification should warn when the order is outside the withdrawal window.' );
+				$this->assertStringContainsString( $warning_message, (string) $merchant_email['message'], 'The merchant email should warn when the order is outside the withdrawal window.' );
+			} else {
+				$this->assertStringNotContainsString( $warning_message, $note->get_content(), 'The inbox notification should not warn when the order is inside the withdrawal window.' );
+				$this->assertStringNotContainsString( $warning_message, (string) $merchant_email['message'], 'The merchant email should not warn when the order is inside the withdrawal window.' );
+			}
+
+			$this->assertStringNotContainsString( $warning_message, (string) $customer_email['message'], 'The customer email should not include the merchant withdrawal window warning.' );
+		} finally {
+			$capture['remove']();
+		}
+	}
+
 	/**
 	 * @testdox Should skip the merchant inbox notification when no order matches.
 	 */