Commit 10a3de13534 for woocommerce
commit 10a3de135347e4a676a5909004f7c5f37757c5f2
Author: Brandon Kraft <public@brandonkraft.com>
Date: Wed Mar 25 15:50:28 2026 +0100
Fix scheduled import preference lost after option rename (#63808)
* Fix scheduled import preference lost after option rename
The `woocommerce_analytics_immediate_import` option was renamed to
`woocommerce_analytics_scheduled_import` in 10.5.0 (185d9c177c23)
with inverted semantics but no migration. Stores that had opted into
scheduled imports silently reverted to immediate imports on upgrade.
Add a runtime fallback in `is_scheduled_import_enabled()` to check the
legacy option (with correct semantic inversion) when the new option is
absent. Add a migration in the 10.7.0 upgrade routine that converts the
legacy value and cleans it up, guarding against write failures by only
deleting the legacy option after a successful `add_option()`.
Fixes WOO6-53
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/fix-scheduled-import-migration-WOO6-53 b/plugins/woocommerce/changelog/fix-scheduled-import-migration-WOO6-53
new file mode 100644
index 00000000000..ad70009387e
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-scheduled-import-migration-WOO6-53
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix scheduled import preference lost after analytics option rename in 10.5.0.
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index a1c0f3bb66c..4f13b39780a 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -325,6 +325,9 @@ class WC_Install {
'10.7.0' => array(
'wc_update_1070_disable_hpos_sync_on_read',
),
+ '10.8.0' => array(
+ 'wc_update_1080_migrate_analytics_import_option',
+ ),
);
/**
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index a307e641b2d..d2178f97928 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -3418,3 +3418,44 @@ function wc_update_1070_disable_hpos_sync_on_read(): void {
WC_Admin_Notices::add_notice( 'hpos_sync_on_read_disabled' );
}
+
+/**
+ * Migrate the legacy analytics immediate import option to the renamed scheduled import option.
+ *
+ * In 10.5.0, `woocommerce_analytics_immediate_import` was renamed to
+ * `woocommerce_analytics_scheduled_import` with inverted semantics, but no
+ * migration was added. Stores that had opted into scheduled imports (legacy
+ * value 'no') silently reverted to immediate imports on upgrade.
+ *
+ * @since 10.8.0
+ *
+ * @return void
+ */
+function wc_update_1080_migrate_analytics_import_option(): void {
+ $legacy_option = 'woocommerce_analytics_immediate_import';
+ $new_option = 'woocommerce_analytics_scheduled_import';
+
+ $legacy_value = get_option( $legacy_option, false );
+
+ // Nothing to migrate if the legacy option was never set.
+ if ( false === $legacy_value ) {
+ return;
+ }
+
+ // If the new option already exists, just clean up the legacy option.
+ if ( false !== get_option( $new_option, false ) ) {
+ delete_option( $legacy_option );
+ return;
+ }
+
+ // Invert the semantics: legacy 'yes' (immediate) = new 'no' (not scheduled),
+ // legacy 'no' (not immediate) = new 'yes' (scheduled).
+ $new_value = 'no' === $legacy_value ? 'yes' : 'no';
+
+ // Only delete the legacy option if the new option was written successfully.
+ // On failure, the runtime fallback in is_scheduled_import_enabled() can
+ // still read the legacy option to preserve the store's preference.
+ if ( add_option( $new_option, $new_value ) ) {
+ delete_option( $legacy_option );
+ }
+}
diff --git a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
index 577f57c7746..ef94a004b7c 100644
--- a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
+++ b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
@@ -60,6 +60,16 @@ class OrdersScheduler extends ImportScheduler {
*/
const SCHEDULED_IMPORT_OPTION = 'woocommerce_analytics_scheduled_import';
+ /**
+ * Legacy option name before the rename in 10.5.0.
+ *
+ * Used as a fallback during upgrades before the migration routine runs.
+ * The old option stored inverted semantics: 'yes' = immediate, 'no' = scheduled.
+ *
+ * @var string
+ */
+ const LEGACY_IMMEDIATE_IMPORT_OPTION = 'woocommerce_analytics_immediate_import';
+
/**
* Default value for the scheduled import option.
*
@@ -772,6 +782,21 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
return false;
}
- return 'yes' === get_option( self::SCHEDULED_IMPORT_OPTION, self::SCHEDULED_IMPORT_OPTION_DEFAULT_VALUE );
+ $value = get_option( self::SCHEDULED_IMPORT_OPTION, false );
+
+ if ( false !== $value ) {
+ return 'yes' === $value;
+ }
+
+ // Fall back to the legacy option (pre-10.5.0) which used inverted semantics:
+ // 'yes' meant immediate import (= not scheduled), 'no' meant scheduled.
+ $legacy_value = get_option( self::LEGACY_IMMEDIATE_IMPORT_OPTION, false );
+
+ if ( false !== $legacy_value ) {
+ return 'no' === $legacy_value;
+ }
+
+ // Neither option exists — use the default (not scheduled).
+ return false;
}
}
diff --git a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
index d671b488874..f082213d01f 100644
--- a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
@@ -272,4 +272,55 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
// Verify the actions were removed.
$this->assertFalse( as_has_scheduled_action( 'fetch_patterns' ), 'fetch_patterns action should be removed after update' );
}
+
+ /**
+ * @testdox Migration converts legacy 'no' (not immediate) to new 'yes' (scheduled).
+ */
+ public function test_migrate_analytics_import_option_legacy_no_becomes_yes(): void {
+ delete_option( 'woocommerce_analytics_scheduled_import' );
+ update_option( 'woocommerce_analytics_immediate_import', 'no' );
+
+ wc_update_1080_migrate_analytics_import_option();
+
+ $this->assertSame( 'yes', get_option( 'woocommerce_analytics_scheduled_import' ) );
+ $this->assertFalse( get_option( 'woocommerce_analytics_immediate_import' ) );
+ }
+
+ /**
+ * @testdox Migration converts legacy 'yes' (immediate) to new 'no' (not scheduled).
+ */
+ public function test_migrate_analytics_import_option_legacy_yes_becomes_no(): void {
+ delete_option( 'woocommerce_analytics_scheduled_import' );
+ update_option( 'woocommerce_analytics_immediate_import', 'yes' );
+
+ wc_update_1080_migrate_analytics_import_option();
+
+ $this->assertSame( 'no', get_option( 'woocommerce_analytics_scheduled_import' ) );
+ $this->assertFalse( get_option( 'woocommerce_analytics_immediate_import' ) );
+ }
+
+ /**
+ * @testdox Migration does nothing when legacy option is absent.
+ */
+ public function test_migrate_analytics_import_option_no_legacy_option(): void {
+ delete_option( 'woocommerce_analytics_immediate_import' );
+ delete_option( 'woocommerce_analytics_scheduled_import' );
+
+ wc_update_1080_migrate_analytics_import_option();
+
+ $this->assertFalse( get_option( 'woocommerce_analytics_scheduled_import' ) );
+ }
+
+ /**
+ * @testdox Migration preserves existing new option and deletes legacy.
+ */
+ public function test_migrate_analytics_import_option_new_option_already_exists(): void {
+ update_option( 'woocommerce_analytics_scheduled_import', 'yes' );
+ update_option( 'woocommerce_analytics_immediate_import', 'yes' );
+
+ wc_update_1080_migrate_analytics_import_option();
+
+ $this->assertSame( 'yes', get_option( 'woocommerce_analytics_scheduled_import' ) );
+ $this->assertFalse( get_option( 'woocommerce_analytics_immediate_import' ) );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
index f9191930378..8ccfc9c123c 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
@@ -35,6 +35,7 @@ class OrdersSchedulerTest extends WC_Unit_Test_Case {
delete_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION );
delete_option( OrdersScheduler::LAST_PROCESSED_ORDER_ID_OPTION );
delete_option( OrdersScheduler::SCHEDULED_IMPORT_OPTION );
+ delete_option( OrdersScheduler::LEGACY_IMMEDIATE_IMPORT_OPTION );
// Clean up any scheduled actions.
$this->clear_scheduled_batch_processor();
@@ -387,6 +388,61 @@ class OrdersSchedulerTest extends WC_Unit_Test_Case {
$this->assertSame( -1, $result );
}
+ /**
+ * @testdox is_scheduled_import_enabled falls back to legacy option when new option is absent.
+ */
+ public function test_is_scheduled_import_enabled_falls_back_to_legacy_option(): void {
+ // Simulate a pre-10.5.0 store that opted into scheduled imports (legacy 'no' = not immediate = scheduled).
+ delete_option( OrdersScheduler::SCHEDULED_IMPORT_OPTION );
+ update_option( OrdersScheduler::LEGACY_IMMEDIATE_IMPORT_OPTION, 'no' );
+
+ $reflection = new \ReflectionClass( OrdersScheduler::class );
+ $method = $reflection->getMethod( 'is_scheduled_import_enabled' );
+ $method->setAccessible( true );
+
+ $this->assertTrue(
+ $method->invoke( null ),
+ 'Legacy option "no" (not immediate) should be interpreted as scheduled import enabled'
+ );
+ }
+
+ /**
+ * @testdox is_scheduled_import_enabled falls back to legacy option 'yes' correctly.
+ */
+ public function test_is_scheduled_import_enabled_falls_back_to_legacy_option_yes(): void {
+ // Simulate a pre-10.5.0 store with default immediate import (legacy 'yes' = immediate = not scheduled).
+ delete_option( OrdersScheduler::SCHEDULED_IMPORT_OPTION );
+ update_option( OrdersScheduler::LEGACY_IMMEDIATE_IMPORT_OPTION, 'yes' );
+
+ $reflection = new \ReflectionClass( OrdersScheduler::class );
+ $method = $reflection->getMethod( 'is_scheduled_import_enabled' );
+ $method->setAccessible( true );
+
+ $this->assertFalse(
+ $method->invoke( null ),
+ 'Legacy option "yes" (immediate) should be interpreted as scheduled import disabled'
+ );
+ }
+
+ /**
+ * @testdox is_scheduled_import_enabled prefers new option over legacy option when they conflict.
+ */
+ public function test_is_scheduled_import_enabled_prefers_new_option(): void {
+ // New option says "not scheduled", legacy says "scheduled" (inverted 'no' = scheduled).
+ // If the new option takes precedence, result should be false.
+ update_option( OrdersScheduler::SCHEDULED_IMPORT_OPTION, 'no' );
+ update_option( OrdersScheduler::LEGACY_IMMEDIATE_IMPORT_OPTION, 'no' );
+
+ $reflection = new \ReflectionClass( OrdersScheduler::class );
+ $method = $reflection->getMethod( 'is_scheduled_import_enabled' );
+ $method->setAccessible( true );
+
+ $this->assertFalse(
+ $method->invoke( null ),
+ 'New option "no" should take precedence over legacy option "no" (which would mean scheduled)'
+ );
+ }
+
/**
* Clear any scheduled batch processor actions.
*