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' );
+	}
 }