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.
 	 *