Commit 8307d25168d for woocommerce
commit 8307d25168d4ec4fe21969e0d9a84c9e3b0ae15f
Author: Justin P <228780+layoutd@users.noreply.github.com>
Date: Wed Oct 14 15:23:47 2026 +0200
Keep Analytics default order status filters applying after a reset or save (#69095)
* Delete Analytics order status options saved at their default
* Test saving Analytics order status lists at and away from the default
* Keep a saved default list when the page fell back from an undisplayable filtered default
diff --git a/plugins/woocommerce/changelog/fix-analytics-settings-keep-filtered-defaults b/plugins/woocommerce/changelog/fix-analytics-settings-keep-filtered-defaults
new file mode 100644
index 00000000000..d63f4e2944e
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-analytics-settings-keep-filtered-defaults
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep the Analytics default order status filters applying after "Reset defaults" or a save that leaves a status list unchanged.
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings.php b/plugins/woocommerce/src/Internal/Admin/Settings.php
index 5b8cfb81b82..3ad158c1e96 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings.php
@@ -49,6 +49,8 @@ class Settings {
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'add_component_settings' ) );
add_filter( 'woocommerce_settings_groups', array( $this, 'add_settings_group' ) );
add_filter( 'woocommerce_settings-wc_admin', array( $this, 'add_settings' ) );
+ add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_excluded_report_order_statuses', array( $this, 'maybe_forget_default_order_statuses' ), 10, 2 );
+ add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_actionable_order_statuses', array( $this, 'maybe_forget_default_order_statuses' ), 10, 2 );
}
/**
@@ -106,8 +108,8 @@ class Settings {
}
/**
- * The default order statuses excluded from Analytics report totals, before a merchant
- * saves the option. Single source of truth for the Settings UI and every runtime
+ * The default order statuses excluded from Analytics report totals, used until a merchant
+ * saves a different selection. Single source of truth for the Settings UI and every runtime
* consumer, so the filtered value applies everywhere without saving the option.
*
* @since 11.2.0
@@ -130,8 +132,8 @@ class Settings {
}
/**
- * The default order statuses considered actionable in Analytics, before a merchant
- * saves the option. Single source of truth for the Settings UI and every runtime
+ * The default order statuses considered actionable in Analytics, used until a merchant
+ * saves a different selection. Single source of truth for the Settings UI and every runtime
* consumer, so the filtered value applies everywhere without saving the option.
*
* @since 11.2.0
@@ -202,6 +204,46 @@ class Settings {
return empty( $statuses ) ? $fallback : $statuses;
}
+ /**
+ * Delete an Analytics order status option instead of saving it when the list matches the
+ * default the Settings page shows.
+ *
+ * "Reset defaults", or saving the page without touching a list, sends that default back.
+ * Storing it would freeze the current value and stop the default order status filters
+ * from applying, so the option is deleted and the default keeps being resolved at runtime.
+ *
+ * @internal
+ *
+ * @since 11.3.0
+ *
+ * @param mixed $value Sanitized value about to be saved.
+ * @param array $option Setting definition, including its resolved default.
+ * @return mixed The value to save, or null to skip saving it.
+ */
+ public function maybe_forget_default_order_statuses( $value, $option ) {
+ $default = $option['default'] ?? null;
+ if ( ! is_array( $value ) || ! is_array( $default ) ) {
+ return $value;
+ }
+
+ // Compare as sets, so the same statuses in another order still count as the default.
+ if ( array_diff( $value, $default ) || array_diff( $default, $value ) ) {
+ return $value;
+ }
+
+ // The page shows the built-in default when a filter returns only statuses it cannot display.
+ // Deleting the option then would switch reports to the filter's list, so store what was shown.
+ $runtime_default = 'woocommerce_excluded_report_order_statuses' === $option['id']
+ ? self::get_default_excluded_order_statuses()
+ : self::get_default_actionable_order_statuses();
+ if ( array_diff( $default, $runtime_default ) ) {
+ return $value;
+ }
+
+ delete_option( $option['id'] );
+ return null;
+ }
+
/**
* Return an object defining the currency options for the site's current currency
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php
index 40abcae32cd..542b018a990 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin;
use Automattic\WooCommerce\Internal\Admin\Settings;
use WC_REST_Setting_Options_Controller;
use WC_Unit_Test_Case;
+use WP_REST_Request;
/**
* Tests for the Settings class.
@@ -332,6 +333,81 @@ class SettingsTest extends WC_Unit_Test_Case {
$this->assertArrayHasKey( 'percent', $settings['couponTypes'], 'Core discount types should still be included.' );
}
+ /**
+ * @testdox Saving an order status list that matches the default deletes the option, so the default filter keeps applying.
+ */
+ public function test_saving_default_order_statuses_keeps_default_filter_applying(): void {
+ $this->add_default_status_filter( 'woocommerce_analytics_settings_default_actionable_order_statuses' );
+ update_option( 'woocommerce_actionable_order_statuses', array( 'processing' ) );
+
+ // What "Reset defaults" sends: the filtered default, here in a different order.
+ $this->save_wc_admin_setting( 'woocommerce_actionable_order_statuses', array( 'refunded', 'on-hold', 'processing' ) );
+
+ $this->assertFalse( get_option( 'woocommerce_actionable_order_statuses' ), 'A list matching the default should not be stored.' );
+
+ remove_filter( 'woocommerce_analytics_settings_default_actionable_order_statuses', array( $this, 'add_status_to_defaults' ) );
+ $setting = $this->find_setting( $this->get_wc_admin_group_settings(), 'woocommerce_actionable_order_statuses' );
+
+ $this->assertSame( array( 'processing', 'on-hold' ), $setting['value'], 'A later change to the default should still reach the setting.' );
+ }
+
+ /**
+ * @testdox Saving the built-in default shown when a filter returns only undisplayable statuses stores it, so reports keep excluding what the page showed.
+ */
+ public function test_saving_fallback_default_stores_it_when_runtime_default_differs(): void {
+ $callback = function () {
+ return array( 'not-a-real-status' );
+ };
+ add_filter( 'woocommerce_analytics_settings_default_excluded_order_statuses', $callback );
+ $this->added_filters[] = array( 'woocommerce_analytics_settings_default_excluded_order_statuses', $callback );
+
+ $setting = $this->find_setting( $this->get_wc_admin_group_settings(), 'woocommerce_excluded_report_order_statuses' );
+ $this->assertSame( array( 'pending', 'cancelled', 'failed' ), $setting['default'], 'The page should fall back to the built-in default.' );
+
+ $this->save_wc_admin_setting( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'cancelled', 'failed' ) );
+
+ $this->assertSame( array( 'pending', 'cancelled', 'failed' ), get_option( 'woocommerce_excluded_report_order_statuses' ), 'The shown list differs from the runtime default, so it must be stored.' );
+ }
+
+ /**
+ * @testdox Saving an order status list that differs from the default stores it.
+ */
+ public function test_saving_non_default_order_statuses_stores_them(): void {
+ $this->save_wc_admin_setting( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed' ) );
+
+ $this->assertSame( array( 'pending', 'failed' ), get_option( 'woocommerce_excluded_report_order_statuses' ) );
+ }
+
+ /**
+ * @testdox Saving an empty order status list stores it, so clearing every status is not undone by the default.
+ */
+ public function test_saving_empty_order_statuses_stores_them(): void {
+ $this->save_wc_admin_setting( 'woocommerce_excluded_report_order_statuses', array() );
+
+ $this->assertSame( array(), get_option( 'woocommerce_excluded_report_order_statuses' ) );
+ }
+
+ /**
+ * Save a wc_admin setting through the REST settings controller, the path the Analytics settings page uses.
+ *
+ * @param string $id Setting id.
+ * @param mixed $value Value to save.
+ */
+ private function save_wc_admin_setting( string $id, $value ): void {
+ $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/wc_admin/' . $id );
+ $request->set_url_params(
+ array(
+ 'group_id' => 'wc_admin',
+ 'id' => $id,
+ )
+ );
+ $request->set_param( 'value', $value );
+
+ $response = ( new WC_REST_Setting_Options_Controller() )->update_item( $request );
+
+ $this->assertNotWPError( $response, 'The setting should save without an error' );
+ }
+
/**
* Get the resolved wc_admin group settings via the REST settings controller.
*