Commit 060af14c4a for woocommerce

commit 060af14c4afed148062f308f2a4deba577849630
Author: Michael Pretty <prettyboymp@users.noreply.github.com>
Date:   Mon Nov 24 11:37:59 2025 -0500

    Fix HPOS to exclude internal statuses when status='any' (#61856)

    * Fix HPOS to exclude internal statuses when status='any'

    Fixes https://github.com/woocommerce/woocommerce/issues/61836

    When HPOS is enabled, the REST API `/wp-json/wc/v3/orders` endpoint
    was returning internal order statuses like 'checkout-draft' when no
    status parameter was provided or when status='any' was used. This
    behavior differs from post-based storage, where WP_Query automatically
    excludes non-public statuses.

    ## Root Cause

    With post-based storage, WooCommerce maps status='any' to post_status='any'
    and relies on WordPress core's WP_Query behavior. When WP_Query receives
    post_status='any', it filters to only "viewable" statuses by excluding
    those with exclude_from_search=true (see wp-includes/class-wp-query.php:2655-2660).

    With HPOS, OrdersTableQuery converts status='any' to an empty array
    (line 700-701), which results in querying ALL statuses without filtering,
    including internal statuses like 'checkout-draft'.

    ## Solution

    Modified `OrdersTableQuery::sanitize_status()` to filter statuses to only
    those marked as visible (`show_in_admin_all_list => true`) when 'any' or
    'all' is requested. This matches:

    1. **WordPress core behavior**: WP_Query's handling of viewable statuses
    2. **Admin Orders ListTable behavior**: Uses the same filtering logic
       (see ListTable.php:540-563)
    3. **Post-based storage behavior**: Relies on WP_Query's automatic filtering

    Internal statuses like 'checkout-draft' are registered with
    `show_in_admin_all_list => false` and are now properly excluded.

    🤖 Generated with [Claude Code](https://claude.com/claude-code)

    Co-Authored-By: Claude <noreply@anthropic.com>

    * Only filter to valid statuses for status = 'any'.

    * add changelog

    * Make sure empty status === 'any' logically

    * Update handling to deal with status => ''

    * lint fixes

    * more lint fixes

    * Messed up lint fix

    ---------

    Co-authored-by: Claude <noreply@anthropic.com>
    Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>

diff --git a/plugins/woocommerce/changelog/fix-61836-hpos-internal-status-filter b/plugins/woocommerce/changelog/fix-61836-hpos-internal-status-filter
new file mode 100644
index 0000000000..05da79347b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-61836-hpos-internal-status-filter
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore original HPOS status filtering behavior to exclude internal order statuses when status='any', matching post-based storage behavior.
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php
index d747c52b64..7dbfb4e5b4 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php
@@ -697,7 +697,11 @@ class OrdersTableQuery {
 			$this->args['status'] = array( $this->args['status'] );
 		}

-		if ( in_array( 'any', $this->args['status'], true ) || in_array( 'all', $this->args['status'], true ) ) {
+		if ( empty( $this->args['status'] ) || in_array( 'any', $this->args['status'], true ) ) {
+			// Querying for 'any' status or empty status, filter to valid statuses from wc_get_order_statuses().
+			$this->args['status'] = $valid_statuses;
+		} elseif ( in_array( 'all', $this->args['status'], true ) ) {
+			// Querying for 'all' status does not filter by status at all.
 			$this->args['status'] = array();
 		}

diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index 1277278375..6473f1735f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -686,6 +686,92 @@ class OrdersTableDataStoreTests extends \HposTestCase {
 		$this->assertEquals( 1, count( $query->orders ) );
 	}

+	/**
+	 * @testDox Tests that 'status' query var handles 'any' and 'all' correctly (excluding/including internal statuses).
+	 *
+	 * @return void
+	 */
+	public function test_cot_query_status_any_and_all() {
+		$this->disable_cot_sync();
+
+		// Create orders with valid WooCommerce statuses.
+		$order_pending = new WC_Order();
+		$this->switch_data_store( $order_pending, $this->sut );
+		$order_pending->set_status( OrderStatus::PENDING );
+		$order_pending->save();
+
+		$order_processing = new WC_Order();
+		$this->switch_data_store( $order_processing, $this->sut );
+		$order_processing->set_status( OrderStatus::PROCESSING );
+		$order_processing->save();
+
+		$order_completed = new WC_Order();
+		$this->switch_data_store( $order_completed, $this->sut );
+		$order_completed->set_status( OrderStatus::COMPLETED );
+		$order_completed->save();
+
+		// Create order with internal WordPress status (auto-draft).
+		$order_auto_draft = new WC_Order();
+		$this->switch_data_store( $order_auto_draft, $this->sut );
+		$order_auto_draft->set_status( OrderStatus::AUTO_DRAFT );
+		$order_auto_draft->save();
+
+		// Create order with checkout-draft status (registered WooCommerce status via DraftOrders service).
+		$order_checkout_draft = new WC_Order();
+		$this->switch_data_store( $order_checkout_draft, $this->sut );
+		$order_checkout_draft->set_status( 'checkout-draft' );
+		$order_checkout_draft->save();
+
+		// Test 'status' => 'any' - should return only valid WooCommerce statuses (excludes internal WordPress statuses like auto-draft).
+		$query = new OrdersTableQuery( array( 'status' => 'any' ) );
+		$this->assertEquals( 4, count( $query->orders ), "status='any' should return only valid WooCommerce statuses" );
+		$this->assertContains( $order_pending->get_id(), $query->orders, "status='any' should include pending orders" );
+		$this->assertContains( $order_processing->get_id(), $query->orders, "status='any' should include processing orders" );
+		$this->assertContains( $order_completed->get_id(), $query->orders, "status='any' should include completed orders" );
+		$this->assertContains( $order_checkout_draft->get_id(), $query->orders, "status='any' should include checkout-draft orders (registered WC status)" );
+		$this->assertNotContains( $order_auto_draft->get_id(), $query->orders, "status='any' should exclude auto-draft orders (internal WordPress status)" );
+
+		// Test 'status' => 'all' - should return all statuses without filtering.
+		$query = new OrdersTableQuery( array( 'status' => 'all' ) );
+		$this->assertEquals( 5, count( $query->orders ), "status='all' should return all orders regardless of status" );
+		$this->assertContains( $order_pending->get_id(), $query->orders, "status='all' should include pending orders" );
+		$this->assertContains( $order_processing->get_id(), $query->orders, "status='all' should include processing orders" );
+		$this->assertContains( $order_completed->get_id(), $query->orders, "status='all' should include completed orders" );
+		$this->assertContains( $order_auto_draft->get_id(), $query->orders, "status='all' should include auto-draft orders" );
+		$this->assertContains( $order_checkout_draft->get_id(), $query->orders, "status='all' should include checkout-draft orders" );
+
+		// Test that internal statuses can still be queried explicitly.
+		$query = new OrdersTableQuery( array( 'status' => OrderStatus::AUTO_DRAFT ) );
+		$this->assertEquals( 1, count( $query->orders ), 'Internal statuses can be queried explicitly' );
+		$this->assertContains( $order_auto_draft->get_id(), $query->orders, 'Explicit query for auto-draft should return auto-draft order' );
+
+		$query = new OrdersTableQuery( array( 'status' => 'checkout-draft' ) );
+		$this->assertEquals( 1, count( $query->orders ), 'Internal statuses can be queried explicitly' );
+		$this->assertContains( $order_checkout_draft->get_id(), $query->orders, 'Explicit query for checkout-draft should return checkout-draft order' );
+
+		// Test with array of statuses including 'any'.
+		$query = new OrdersTableQuery( array( 'status' => array( 'any' ) ) );
+		$this->assertEquals( 4, count( $query->orders ), "status=['any'] should work same as status='any'" );
+
+		// Test empty status (should behave like 'any') - historical and WP_Query like behavior.
+		$query = new OrdersTableQuery( array( 'status' => '' ) );
+		$this->assertEquals( 4, count( $query->orders ), "Empty status should behave like 'any' and return only valid WooCommerce statuses" );
+		$this->assertContains( $order_pending->get_id(), $query->orders, 'Empty status should include pending orders' );
+		$this->assertContains( $order_processing->get_id(), $query->orders, 'Empty status should include processing orders' );
+		$this->assertContains( $order_completed->get_id(), $query->orders, 'Empty status should include completed orders' );
+		$this->assertContains( $order_checkout_draft->get_id(), $query->orders, 'Empty status should include checkout-draft orders (registered WC status)' );
+		$this->assertNotContains( $order_auto_draft->get_id(), $query->orders, 'Empty status should exclude auto-draft orders (internal WordPress status)' );
+
+		// Test omitted status (should behave like 'any').
+		$query = new OrdersTableQuery( array() );
+		$this->assertEquals( 4, count( $query->orders ), "Omitted status should behave like 'any' and return only valid WooCommerce statuses" );
+		$this->assertContains( $order_pending->get_id(), $query->orders, 'Omitted status should include pending orders' );
+		$this->assertContains( $order_processing->get_id(), $query->orders, 'Omitted status should include processing orders' );
+		$this->assertContains( $order_completed->get_id(), $query->orders, 'Omitted status should include completed orders' );
+		$this->assertContains( $order_checkout_draft->get_id(), $query->orders, 'Omitted status should include checkout-draft orders (registered WC status)' );
+		$this->assertNotContains( $order_auto_draft->get_id(), $query->orders, 'Omitted status should exclude auto-draft orders (internal WordPress status)' );
+	}
+
 	/**
 	 * @testDox Tests meta queries in the `OrdersTableQuery` class.
 	 *