Commit 3d8d6bf87dd for woocommerce

commit 3d8d6bf87dd5aa0aba50a87d6161f0778f85c13e
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date:   Fri Jul 10 15:29:50 2026 +0800

    fix: respect cleared actionable order statuses in Activity Panel counts (#66483)

    * fix(activity-panel): respect cleared actionable order statuses in counts endpoint

    * refactor(activity-panel): drop redundant fallback guard, expand counts test coverage

diff --git a/plugins/woocommerce/changelog/fix-66480-empty-actionable-order-statuses b/plugins/woocommerce/changelog/fix-66480-empty-actionable-order-statuses
new file mode 100644
index 00000000000..ecb6c1102fe
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-66480-empty-actionable-order-statuses
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Activity Panel counts endpoint now respects an explicitly-empty actionable order statuses setting instead of falling back to the defaults, so a cleared setting reports zero orders to fulfill.
diff --git a/plugins/woocommerce/src/Admin/API/ActivityPanelCounts.php b/plugins/woocommerce/src/Admin/API/ActivityPanelCounts.php
index 519d6f07924..53dc4f13d00 100644
--- a/plugins/woocommerce/src/Admin/API/ActivityPanelCounts.php
+++ b/plugins/woocommerce/src/Admin/API/ActivityPanelCounts.php
@@ -59,17 +59,27 @@ class ActivityPanelCounts extends \WC_REST_Data_Controller {
 	 * @return \WP_REST_Response
 	 */
 	public function get_counts( $request ) {
+		$order_statuses = (array) $request->get_param( 'order_statuses' );
+
+		// When a merchant has cleared every actionable order status there is nothing
+		// "to fulfill". Short-circuit to 0 rather than querying: an empty status list
+		// would otherwise count every order, and the previous client-side
+		// getUnreadOrders() returned 0 in this case.
+		$orders_to_fulfill_count = empty( $order_statuses )
+			? 0
+			: $this->get_count_via(
+				'/wc-analytics/orders',
+				array(
+					'page'     => 1,
+					'per_page' => 1,
+					'status'   => $order_statuses,
+					'_fields'  => array( 'id' ),
+				)
+			);
+
 		return rest_ensure_response(
 			array(
-				'orders_to_fulfill_count'     => $this->get_count_via(
-					'/wc-analytics/orders',
-					array(
-						'page'     => 1,
-						'per_page' => 1,
-						'status'   => $request->get_param( 'order_statuses' ),
-						'_fields'  => array( 'id' ),
-					)
-				),
+				'orders_to_fulfill_count'     => $orders_to_fulfill_count,
 				'reviews_to_moderate_count'   => $this->get_count_via(
 					'/wc-analytics/products/reviews',
 					array(
@@ -161,8 +171,13 @@ class ActivityPanelCounts extends \WC_REST_Data_Controller {
 	 * @return array
 	 */
 	private function get_default_order_statuses() {
-		$actionable = get_option( 'woocommerce_actionable_order_statuses', array() );
-		return is_array( $actionable ) && ! empty( $actionable ) ? $actionable : array( 'processing', 'on-hold' );
+		$actionable = get_option( 'woocommerce_actionable_order_statuses', false );
+
+		// Any array is respected as-is, including an explicitly empty one: the merchant
+		// intentionally cleared all actionable statuses, so there is nothing to fulfill,
+		// matching the previous client-side behaviour. A missing (never configured) or
+		// malformed option falls back to the built-in defaults.
+		return is_array( $actionable ) ? $actionable : array( 'processing', 'on-hold' );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/ActivityPanelCountsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/ActivityPanelCountsTest.php
index b27469fec97..f354e3d62de 100644
--- a/plugins/woocommerce/tests/php/src/Admin/API/ActivityPanelCountsTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/API/ActivityPanelCountsTest.php
@@ -16,6 +16,7 @@ use WC_Helper_Product;
 use WC_REST_Unit_Test_Case;
 use WP_Error;
 use WP_REST_Request;
+use WP_REST_Server;

 /**
  * ActivityPanelCounts API controller test.
@@ -181,4 +182,66 @@ class ActivityPanelCountsTest extends WC_REST_Unit_Test_Case {
 		$this->assertNull( $data['products_low_in_stock_count'] );
 		$this->assertEquals( 1, $data['orders_to_fulfill_count'] );
 	}
+
+	/**
+	 * Re-register REST routes on a fresh server. The endpoint's order_statuses default is
+	 * computed at registration time (which setUp() has already run), so tests that change
+	 * the actionable statuses option must re-register routes for the default to pick it up,
+	 * as it would on a real request.
+	 */
+	private function reregister_routes() {
+		$this->server              = new WP_REST_Server();
+		$GLOBALS['wp_rest_server'] = $this->server;
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Re-firing a core hook to re-register routes in the test, not defining one.
+		do_action( 'rest_api_init' );
+	}
+
+	/**
+	 * Test that clearing all actionable order statuses yields a 0 orders-to-fulfill count,
+	 * matching the previous client-side getUnreadOrders() behaviour, rather than falling back
+	 * to the default statuses. Mirrors the client, which calls the endpoint without passing
+	 * order_statuses and relies on the endpoint default.
+	 *
+	 * Option changes are rolled back by the per-test DB transaction, so no manual cleanup.
+	 */
+	public function test_cleared_actionable_statuses_yield_zero_orders_to_fulfill() {
+		wp_set_current_user( $this->user );
+
+		WC_Helper_Order::create_order( 1, null, array( 'status' => OrderStatus::PROCESSING ) );
+
+		// Merchant cleared every actionable status.
+		update_option( 'woocommerce_actionable_order_statuses', array() );
+		$this->reregister_routes();
+
+		$request  = new WP_REST_Request( 'GET', self::ENDPOINT );
+		$response = $this->server->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertSame( 0, $data['orders_to_fulfill_count'] );
+	}
+
+	/**
+	 * Test that a merchant's custom (non-empty) actionable statuses selection drives the
+	 * endpoint's default orders-to-fulfill count when the client passes no order_statuses
+	 * param. Counts are asymmetric (2 completed vs 1 processing) so falling back to the
+	 * built-in processing/on-hold defaults would fail the assertion.
+	 */
+	public function test_custom_actionable_statuses_option_drives_default_count() {
+		wp_set_current_user( $this->user );
+
+		WC_Helper_Order::create_order( 1, null, array( 'status' => OrderStatus::PROCESSING ) );
+		WC_Helper_Order::create_order( 1, null, array( 'status' => OrderStatus::COMPLETED ) );
+		WC_Helper_Order::create_order( 1, null, array( 'status' => OrderStatus::COMPLETED ) );
+
+		update_option( 'woocommerce_actionable_order_statuses', array( OrderStatus::COMPLETED ) );
+		$this->reregister_routes();
+
+		$request  = new WP_REST_Request( 'GET', self::ENDPOINT );
+		$response = $this->server->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertSame( 2, $data['orders_to_fulfill_count'] );
+	}
 }