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.'
+ );
+ }
}