Commit 2790abec76c for woocommerce

commit 2790abec76c388cb36914ef8dcc5340cef660254
Author: Anand Rajaram <anandrajaram21@gmail.com>
Date:   Thu Sep 24 07:36:31 2026 -0700

    Fix unpaid order cancellation schedule resets (#68310)

    Fix unpaid order cancellation schedule resets on settings save

    - Keep the pending woocommerce_cancel_unpaid_orders action when the hold-stock value is unchanged
    - Recreate the action when it is missing and reschedule it when the value changes
    - Read the hold-stock option with its '60' default so blank values are handled
    - Add regression tests for unchanged, changed, blank, and missing-action cases

diff --git a/plugins/woocommerce/changelog/55310-preserve-unpaid-order-cancellation-schedule b/plugins/woocommerce/changelog/55310-preserve-unpaid-order-cancellation-schedule
new file mode 100644
index 00000000000..fa1ff9db4e2
--- /dev/null
+++ b/plugins/woocommerce/changelog/55310-preserve-unpaid-order-cancellation-schedule
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Preserve the unpaid order cancellation schedule when saving unchanged hold stock settings.
diff --git a/plugins/woocommerce/includes/wc-formatting-functions.php b/plugins/woocommerce/includes/wc-formatting-functions.php
index a09c43640c5..c3032016683 100644
--- a/plugins/woocommerce/includes/wc-formatting-functions.php
+++ b/plugins/woocommerce/includes/wc-formatting-functions.php
@@ -1233,7 +1233,6 @@ add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_de
 /**
  * Formats hold stock option and sets cron event up.
  *
- * @codeCoverageIgnore
  * @param  string $value     Option value.
  * @param  array  $option    Option name.
  * @param  string $raw_value Raw value.
@@ -1242,14 +1241,19 @@ add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_de
 function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) {
 	$value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''.

+	$value_changed         = (string) get_option( 'woocommerce_hold_stock_minutes', '60' ) !== (string) $value;
+	$next_scheduled_action = function_exists( 'as_next_scheduled_action' )
+		? as_next_scheduled_action( 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce' )
+		: wp_next_scheduled( 'woocommerce_cancel_unpaid_orders' );
+
 	// Clear existing scheduled events.
-	if ( function_exists( 'as_unschedule_all_actions' ) ) {
+	if ( $value_changed && function_exists( 'as_unschedule_all_actions' ) ) {
 		as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
-	} else {
+	} elseif ( $value_changed ) {
 		wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
 	}

-	if ( '' !== $value ) {
+	if ( '' !== $value && ( $value_changed || false === $next_scheduled_action ) ) {
 		/**
 		 * Filters the interval at which to cancel unpaid orders in minutes.
 		 *
diff --git a/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php
index 22abf9b9479..6f0f71b1d5b 100644
--- a/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php
@@ -233,4 +233,75 @@ class WC_Formatting_Functions_Test extends \WC_Unit_Test_Case {
 		add_filter( 'woocommerce_stock_amount', 'intval' );
 		$this->assertTrue( wc_is_stock_amount_integer(), 'Should return true when intval is applied to stock amount filter.' );
 	}
+
+	/**
+	 * @testdox Saving an unchanged hold stock value should preserve the pending cancellation action.
+	 */
+	public function test_unchanged_hold_stock_value_preserves_pending_cancellation_action(): void {
+		$scheduled_time = time() + ( 2 * HOUR_IN_SECONDS );
+
+		update_option( 'woocommerce_hold_stock_minutes', '60' );
+		as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
+		as_schedule_single_action( $scheduled_time, 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce' );
+
+		wc_format_option_hold_stock_minutes( '60', array(), '60' );
+
+		$this->assertSame(
+			$scheduled_time,
+			as_next_scheduled_action( 'woocommerce_cancel_unpaid_orders' ),
+			'An unchanged hold stock value should not delay the existing cancellation action.'
+		);
+	}
+
+	/**
+	 * @testdox Saving an unchanged hold stock value should recreate a missing cancellation action.
+	 */
+	public function test_unchanged_hold_stock_value_recreates_missing_cancellation_action(): void {
+		update_option( 'woocommerce_hold_stock_minutes', '60' );
+		as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
+
+		$expected_time = time() + HOUR_IN_SECONDS;
+		wc_format_option_hold_stock_minutes( '60', array(), '60' );
+
+		$this->assertEqualsWithDelta(
+			$expected_time,
+			as_next_scheduled_action( 'woocommerce_cancel_unpaid_orders' ),
+			1,
+			'An unchanged hold stock value should recreate a missing cancellation action.'
+		);
+	}
+
+	/**
+	 * @testdox Saving a changed hold stock value should replace the cancellation action at the new interval.
+	 */
+	public function test_changed_hold_stock_value_replaces_cancellation_action(): void {
+		$scheduled_time = time() + ( 2 * HOUR_IN_SECONDS );
+
+		update_option( 'woocommerce_hold_stock_minutes', '60' );
+		as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
+		as_schedule_single_action( $scheduled_time, 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce' );
+
+		$expected_time = time() + ( 30 * MINUTE_IN_SECONDS );
+		wc_format_option_hold_stock_minutes( '30', array(), '30' );
+		$next_scheduled_time = as_next_scheduled_action( 'woocommerce_cancel_unpaid_orders' );
+
+		$this->assertNotSame( $scheduled_time, $next_scheduled_time, 'A changed hold stock value should replace the existing cancellation action.' );
+		$this->assertEqualsWithDelta( $expected_time, $next_scheduled_time, 1, 'The replacement action should use the new hold stock interval.' );
+	}
+
+	/**
+	 * @testdox Saving a blank hold stock value should remove the cancellation action when the option is missing.
+	 */
+	public function test_blank_hold_stock_value_removes_cancellation_action_when_option_is_missing(): void {
+		delete_option( 'woocommerce_hold_stock_minutes' );
+		as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
+		as_schedule_single_action( time() + HOUR_IN_SECONDS, 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce' );
+
+		wc_format_option_hold_stock_minutes( '', array(), '' );
+
+		$this->assertFalse(
+			as_next_scheduled_action( 'woocommerce_cancel_unpaid_orders' ),
+			'A blank hold stock value should remove the cancellation action.'
+		);
+	}
 }