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