Commit a5c25eff65e for woocommerce
commit a5c25eff65e6491289b040c541c537e254b2b191
Author: Peter Petrov <peter.petrov89@gmail.com>
Date: Tue Aug 11 18:29:46 2026 +0300
Honor order_date_type and day-granularity date filters on the HPOS orders list (#67625)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/fix-41318-hpos-orders-date-filter b/plugins/woocommerce/changelog/fix-41318-hpos-orders-date-filter
new file mode 100644
index 00000000000..973401b4464
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-41318-hpos-orders-date-filter
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix date filtering of the HPOS orders list via links from Analytics reports (order_date_type and day-granularity date params were ignored).
diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php
index 7218ce51511..9b5b3943686 100644
--- a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php
+++ b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php
@@ -504,25 +504,43 @@ class ListTable extends WP_List_Table {
}
/**
- * Implements date (month-based) filtering.
+ * Implements date filtering.
+ *
+ * The 'm' query arg is accepted at month (YYYYMM) or day (YYYYMMDD) granularity. The date field being filtered
+ * defaults to 'date_created' and can be changed via the 'order_date_type' query arg, which is how Analytics
+ * reports link to orders paid or completed on a given day.
*/
private function set_date_args() {
- $year_month = sanitize_text_field( wp_unslash( $_GET['m'] ?? '' ) );
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
+ $date_value = sanitize_text_field( wp_unslash( $_GET['m'] ?? '' ) );
- if ( empty( $year_month ) || ! preg_match( '/^[0-9]{6}$/', $year_month ) ) {
+ if ( empty( $date_value ) || ! preg_match( '/^([0-9]{4})([0-9]{2})([0-9]{2})?$/', $date_value, $matches ) ) {
return;
}
- $year = (int) substr( $year_month, 0, 4 );
- $month = (int) substr( $year_month, 4, 2 );
+ $year = (int) $matches[1];
+ $month = (int) $matches[2];
+ $day = isset( $matches[3] ) ? (int) $matches[3] : null;
- if ( $month < 0 || $month > 12 ) {
+ if ( ! checkdate( $month, $day ?? 1, $year ) ) {
return;
}
- $last_day_of_month = date_create( "$year-$month" )->format( 'Y-m-t' );
- $this->order_query_args['date_created'] = "$year-$month-01..." . $last_day_of_month;
- $this->has_filter = true;
+ $date_type = sanitize_text_field( wp_unslash( $_GET['order_date_type'] ?? '' ) );
+ if ( ! in_array( $date_type, array( 'date_created', 'date_paid', 'date_completed' ), true ) ) {
+ $date_type = 'date_created';
+ }
+
+ if ( is_null( $day ) ) {
+ $date_start = sprintf( '%04d-%02d-01', $year, $month );
+ $date_end = date_create( $date_start )->format( 'Y-m-t' );
+ } else {
+ $date_start = sprintf( '%04d-%02d-%02d', $year, $month, $day );
+ $date_end = $date_start;
+ }
+
+ $this->order_query_args[ $date_type ] = "$date_start...$date_end";
+ $this->has_filter = true;
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
index 6e0a44349dc..7daddbd35c7 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
@@ -358,4 +358,138 @@ class ListTableTest extends \WC_Unit_Test_Case {
$this->assertTrue( $query_args['no_found_rows'] ?? false, 'A basic query should use the cache fast path' );
}
+
+ /**
+ * Create two paid orders with all date props set to the given dates.
+ *
+ * @param string $date1 Date for the first order.
+ * @param string $date2 Date for the second order.
+ *
+ * @return \WC_Order[]
+ */
+ private function create_orders_dated( string $date1, string $date2 ): array {
+ $orders = array();
+
+ foreach ( array( $date1, $date2 ) as $date ) {
+ $order = \WC_Helper_Order::create_order();
+ $order->set_date_created( $date );
+ $order->set_date_paid( $date );
+ $order->set_date_completed( $date );
+ $order->save();
+ $orders[] = $order;
+ }
+
+ return $orders;
+ }
+
+ /**
+ * @testdox Filtering by a specific day and order_date_type=date_paid (as Analytics revenue links do) shows only orders paid that day.
+ */
+ public function test_filtering_by_day_and_date_paid_shows_only_matching_orders(): void {
+ list( $order1, ) = $this->create_orders_dated( '2023-10-25 10:00:00', '2023-09-01 10:00:00' );
+
+ $_GET['m'] = '20231025';
+ $_GET['order_date_type'] = 'date_paid';
+
+ $this->sut->prepare_items();
+ $query_args = $this->get_order_query_args();
+
+ $get_items = function () {
+ return $this->items;
+ };
+ $items = $get_items->call( $this->sut );
+
+ unset( $_GET['m'], $_GET['order_date_type'] );
+
+ $this->assertSame( '2023-10-25...2023-10-25', $query_args['date_paid'] ?? null, 'A day-granularity filter should query the full day on date_paid' );
+ $this->assertCount( 1, $items, 'Only the order paid on the selected day should be shown' );
+ $this->assertEquals( $order1->get_id(), $items[0]->get_id() );
+ }
+
+ /**
+ * @testdox Filtering by a specific day without order_date_type filters on date_created.
+ */
+ public function test_filtering_by_day_defaults_to_date_created(): void {
+ list( $order1, ) = $this->create_orders_dated( '2023-10-25 10:00:00', '2023-09-01 10:00:00' );
+
+ $_GET['m'] = '20231025';
+
+ $this->sut->prepare_items();
+ $query_args = $this->get_order_query_args();
+
+ $get_items = function () {
+ return $this->items;
+ };
+ $items = $get_items->call( $this->sut );
+
+ unset( $_GET['m'] );
+
+ $this->assertSame( '2023-10-25...2023-10-25', $query_args['date_created'] ?? null, 'A day-granularity filter should default to date_created' );
+ $this->assertCount( 1, $items, 'Only the order created on the selected day should be shown' );
+ $this->assertEquals( $order1->get_id(), $items[0]->get_id() );
+ }
+
+ /**
+ * @testdox Filtering by month keeps working and honors order_date_type.
+ */
+ public function test_filtering_by_month_honors_order_date_type(): void {
+ list( $order1, ) = $this->create_orders_dated( '2023-10-25 10:00:00', '2023-09-01 10:00:00' );
+
+ $_GET['m'] = '202310';
+ $_GET['order_date_type'] = 'date_completed';
+
+ $this->sut->prepare_items();
+ $query_args = $this->get_order_query_args();
+
+ $get_items = function () {
+ return $this->items;
+ };
+ $items = $get_items->call( $this->sut );
+
+ unset( $_GET['m'], $_GET['order_date_type'] );
+
+ $this->assertSame( '2023-10-01...2023-10-31', $query_args['date_completed'] ?? null, 'A month-granularity filter should query the full month on the requested date type' );
+ $this->assertCount( 1, $items, 'Only the order completed in the selected month should be shown' );
+ $this->assertEquals( $order1->get_id(), $items[0]->get_id() );
+ }
+
+ /**
+ * @testdox An unrecognized order_date_type falls back to filtering on date_created.
+ */
+ public function test_filtering_with_invalid_order_date_type_falls_back_to_date_created(): void {
+ $this->create_orders_dated( '2023-10-25 10:00:00', '2023-09-01 10:00:00' );
+
+ $_GET['m'] = '20231025';
+ $_GET['order_date_type'] = 'date_hacked';
+
+ $this->sut->prepare_items();
+ $query_args = $this->get_order_query_args();
+
+ unset( $_GET['m'], $_GET['order_date_type'] );
+
+ $this->assertSame( '2023-10-25...2023-10-25', $query_args['date_created'] ?? null, 'Unknown date types should fall back to date_created' );
+ $this->assertArrayNotHasKey( 'date_hacked', $query_args, 'Unknown date types should never reach the query args' );
+ }
+
+ /**
+ * @testdox An invalid date value in the m query arg is ignored.
+ */
+ public function test_filtering_with_invalid_date_is_ignored(): void {
+ $this->create_orders_dated( '2023-10-25 10:00:00', '2023-09-01 10:00:00' );
+
+ $_GET['m'] = '20231399';
+
+ $this->sut->prepare_items();
+ $query_args = $this->get_order_query_args();
+
+ $get_items = function () {
+ return $this->items;
+ };
+ $items = $get_items->call( $this->sut );
+
+ unset( $_GET['m'] );
+
+ $this->assertArrayNotHasKey( 'date_created', $query_args, 'An invalid date should not produce a date filter' );
+ $this->assertCount( 2, $items, 'An invalid date filter should be ignored and all orders shown' );
+ }
}