Commit f2829f88203 for woocommerce

commit f2829f88203d952c532a4358e427c23f53fbd8f0
Author: Saskia Teichmann <s-a-s-k-i-a@users.noreply.github.com>
Date:   Mon Sep 14 14:41:19 2026 +0200

    Rebuild the order count cache when a status is registered after the cache was primed (#68114)

    * Rebuild order count cache when a status is registered after priming

    The order count cache saves the set of statuses it was primed with and
    serves cache hits as long as those keys are warm. A custom order status
    registered afterwards (e.g. by a just-activated plugin) was therefore
    missing from the Orders screen filter row for up to a day, and orders
    moved into it kept inflating the count of their previous status, because
    the incremental cache updates skip statuses that are not cached.

    OrderUtil::get_count_for_type() now treats a cached set that lacks a
    currently registered status as a cache miss and rebuilds the counts
    from the database. Cached statuses that are not registered are still
    served unchanged for backward compatibility.

    Fixes #68009

    * Harden registered-status read against non-array filter results

    Address review: coerce the wc_order_statuses filter result before
    array_keys() on the warm-cache path so a broken filter callback cannot
    fatal there, and add a warm-cache regression test.

    * Flush order counts on plugin (de)activation instead of a staleness check

    Address review: drop the registered-status comparison from
    OrderUtil::get_count_for_type() and instead flush the order count
    cache from OrderCountCacheService when a plugin is activated or
    deactivated, keeping the cache logic in its own context. Tests and
    changelog updated accordingly.

    * Register the custom status as a post status in the regression test

    Real plugins register their custom order status via register_post_status()
    in addition to the wc_order_statuses filter; without the registration the
    HPOS orders table stores the status unprefixed and the count lands under
    a different key, which made the test assert against the wrong scenario.

    * Polish: compact changelog, shorter docblock, hook-focused activation test

    Address review: the changelog entry is shortened for readability, the
    flush_cache() docblock only describes what the method does, and the
    activation test now mirrors the deactivation test — it verifies that the
    hook flushes the cache without exercising custom status handling.

    * Add missing blank line between test methods

    ---------

    Co-authored-by: Vladimir Reznichenko <kalessil@gmail.com>

diff --git a/plugins/woocommerce/changelog/68009-order-count-cache-late-registered-status b/plugins/woocommerce/changelog/68009-order-count-cache-late-registered-status
new file mode 100644
index 00000000000..3ae107aef73
--- /dev/null
+++ b/plugins/woocommerce/changelog/68009-order-count-cache-late-registered-status
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Flush the order count cache when a plugin is activated or deactivated, so newly registered order statuses show up in the Orders list right away.
diff --git a/plugins/woocommerce/src/Caches/OrderCountCacheService.php b/plugins/woocommerce/src/Caches/OrderCountCacheService.php
index bcf6f787d16..f72b5fba0f1 100644
--- a/plugins/woocommerce/src/Caches/OrderCountCacheService.php
+++ b/plugins/woocommerce/src/Caches/OrderCountCacheService.php
@@ -52,6 +52,8 @@ class OrderCountCacheService {
 		add_action( 'woocommerce_before_trash_order', array( $this, 'update_on_order_trashed' ), 10, 2 );
 		add_action( 'woocommerce_before_delete_order', array( $this, 'update_on_order_deleted' ), 10, 2 );
 		add_action( self::BACKGROUND_EVENT_HOOK, array( $this, 'prime_cache_if_cold' ) );
+		add_action( 'activated_plugin', array( $this, 'flush_cache' ) );
+		add_action( 'deactivated_plugin', array( $this, 'flush_cache' ) );
 		add_action( 'action_scheduler_ensure_recurring_actions', array( $this, 'schedule_background_actions' ) );

 		if ( defined( 'WC_PLUGIN_BASENAME' ) ) {
@@ -93,6 +95,20 @@ class OrderCountCacheService {
 		}
 	}

+	/**
+	 * Flush the order count cache for all order types.
+	 *
+	 * @internal
+	 * @since 11.2.0
+	 *
+	 * @return void
+	 */
+	public function flush_cache() {
+		foreach ( wc_get_order_types( 'order-count' ) as $order_type ) {
+			$this->order_count_cache->flush( $order_type );
+		}
+	}
+
 	/**
 	 * Register background caching for each order type.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php b/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php
index cf06a74afdb..f9485fc662c 100644
--- a/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php
+++ b/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php
@@ -215,4 +215,28 @@ class OrderCountCacheServiceTest extends \WC_Unit_Test_Case {

 		$this->assertNull( $this->order_cache->get( 'shop_order', array( OrderInternalStatus::PENDING ) ) );
 	}
+
+	/**
+	 * Test that activating a plugin flushes the order count cache.
+	 */
+	public function test_activated_plugin_flushes_cache(): void {
+		OrderUtil::get_count_for_type( 'shop_order' );
+		$this->assertNotNull( $this->order_cache->get( 'shop_order' ) );
+
+		do_action( 'activated_plugin', 'custom-status-plugin/custom-status-plugin.php', false );
+
+		$this->assertNull( $this->order_cache->get( 'shop_order' ) );
+	}
+
+	/**
+	 * Test that deactivating a plugin flushes the order count cache.
+	 */
+	public function test_deactivated_plugin_flushes_cache(): void {
+		OrderUtil::get_count_for_type( 'shop_order' );
+		$this->assertNotNull( $this->order_cache->get( 'shop_order' ) );
+
+		do_action( 'deactivated_plugin', 'custom-status-plugin/custom-status-plugin.php', false );
+
+		$this->assertNull( $this->order_cache->get( 'shop_order' ) );
+	}
 }