Commit ba80f14f5fa for woocommerce

commit ba80f14f5fa6116e79cb4c87b314bfa591d73021
Author: Akram ul haq <kamig478@gmail.com>
Date:   Wed Nov 4 15:32:09 2026 +0500

    Fix variations stats ignoring Analytics products filter (#70609)

    * Fix variations stats ignoring Analytics products filter

    Map the products query alias to product_includes on reports/variations/stats so single-product Variation table totals match the filtered rows (#70316).

    * Fix PHPCS lint on variations stats products filter

    Add strict_types to the new API test and realign assignment spacing flagged by lint.

    * Align variations stats collection params with neighboring assignments

    Match Generic.Formatting.MultipleStatementAlignment with the product_includes group so lint passes on the latest commit.

    * Fix file header order in variations stats API test

    Put the file docblock immediately after the opening PHP tag, then declare(strict_types=1), matching other legacy API tests.

diff --git a/plugins/woocommerce/changelog/fix-70316-variations-stats-products-param b/plugins/woocommerce/changelog/fix-70316-variations-stats-products-param
new file mode 100644
index 00000000000..f3af322b0c6
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-70316-variations-stats-products-param
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Map the Analytics `products` filter on variations stats so single-product Variation table totals match the rows.
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/Controller.php
index 7bcd87f657b..258e25da896 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/Controller.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/Controller.php
@@ -36,6 +36,7 @@ class Controller extends GenericStatsController {
 	 */
 	protected $param_mapping = array(
 		'variations' => 'variation_includes',
+		'products'   => 'product_includes',
 	);

 	/**
@@ -261,6 +262,15 @@ class Controller extends GenericStatsController {
 			'validate_callback' => 'rest_validate_request_arg',
 			'sanitize_callback' => 'wp_parse_id_list',
 		);
+		$params['products']          = array(
+			'description'       => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
+			'type'              => 'array',
+			'sanitize_callback' => 'wp_parse_id_list',
+			'validate_callback' => 'rest_validate_request_arg',
+			'items'             => array(
+				'type' => 'integer',
+			),
+		);
 		$params['variations']        = array(
 			'description'       => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
 			'type'              => 'array',
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-variations-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-variations-stats.php
new file mode 100644
index 00000000000..7a65e43fcc0
--- /dev/null
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-variations-stats.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Reports Variations Stats REST API Test
+ *
+ * @package WooCommerce\Admin\Tests\API
+ */
+
+declare(strict_types=1);
+
+use Automattic\WooCommerce\Enums\OrderStatus;
+
+/**
+ * Class WC_Admin_Tests_API_Reports_Variations_Stats
+ */
+class WC_Admin_Tests_API_Reports_Variations_Stats extends WC_REST_Unit_Test_Case {
+
+	/**
+	 * Endpoints.
+	 *
+	 * @var string
+	 */
+	protected $endpoint = '/wc-analytics/reports/variations/stats';
+
+	/**
+	 * Setup test reports variations stats data.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->user = $this->factory->user->create(
+			array(
+				'role' => 'administrator',
+			)
+		);
+	}
+
+	/**
+	 * Test route registration.
+	 */
+	public function test_register_routes() {
+		// This namespace may be lazy loaded, so we make a discovery request to trigger loading for this test.
+		$this->server->dispatch( new WP_REST_Request( 'GET', '/' ) );
+		$routes = $this->server->get_routes();
+
+		$this->assertArrayHasKey( $this->endpoint, $routes );
+	}
+
+	/**
+	 * Create a variable product with one variation and a completed order for it.
+	 *
+	 * @param string $name  Product name.
+	 * @param float  $price Variation regular price.
+	 * @return array{product: WC_Product_Variable, variation: WC_Product_Variation}
+	 */
+	private function create_variable_product_with_order( $name, $price ) {
+		$product = new WC_Product_Variable();
+		$product->set_name( $name );
+		$product->save();
+
+		$variation = new WC_Product_Variation();
+		$variation->set_parent_id( $product->get_id() );
+		$variation->set_regular_price( $price );
+		$variation->set_attributes( array( 'color' => 'green' ) );
+		$variation->save();
+
+		$order = WC_Helper_Order::create_order( 1, $variation );
+		$order->set_status( OrderStatus::COMPLETED );
+		$order->set_total( $price * 4 );
+		$order->save();
+
+		return array(
+			'product'   => $product,
+			'variation' => $variation,
+		);
+	}
+
+	/**
+	 * @testdox The products alias should filter variation stats the same way product_includes does.
+	 */
+	public function test_products_param_filters_totals_like_product_includes() {
+		wp_set_current_user( $this->user );
+		WC_Helper_Reports::reset_stats_dbs();
+
+		$time = time();
+
+		$first  = $this->create_variable_product_with_order( 'First Variable', 25 );
+		$second = $this->create_variable_product_with_order( 'Second Variable', 50 );
+
+		WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+		$base_params = array(
+			'before'   => gmdate( 'Y-m-d 23:59:59', $time ),
+			'after'    => gmdate( 'Y-m-d 00:00:00', $time ),
+			'interval' => 'day',
+		);
+
+		$unfiltered_request = new WP_REST_Request( 'GET', $this->endpoint );
+		$unfiltered_request->set_query_params( $base_params );
+		$unfiltered = $this->server->dispatch( $unfiltered_request )->get_data();
+
+		$this->assertEquals( 8, $unfiltered['totals']['items_sold'], 'Unfiltered stats should include both products.' );
+		$this->assertEquals( 2, $unfiltered['totals']['orders_count'] );
+
+		$products_request = new WP_REST_Request( 'GET', $this->endpoint );
+		$products_request->set_query_params(
+			array_merge(
+				$base_params,
+				array(
+					'products' => array( $first['product']->get_id() ),
+				)
+			)
+		);
+		$products_response = $this->server->dispatch( $products_request );
+		$products_filtered = $products_response->get_data();
+
+		$includes_request = new WP_REST_Request( 'GET', $this->endpoint );
+		$includes_request->set_query_params(
+			array_merge(
+				$base_params,
+				array(
+					'product_includes' => array( $first['product']->get_id() ),
+				)
+			)
+		);
+		$includes_filtered = $this->server->dispatch( $includes_request )->get_data();
+
+		$this->assertEquals( 200, $products_response->get_status() );
+		$this->assertEquals(
+			4,
+			$products_filtered['totals']['items_sold'],
+			'products should limit stats to the selected parent product.'
+		);
+		$this->assertEquals( 1, $products_filtered['totals']['orders_count'] );
+		$this->assertEquals(
+			$includes_filtered['totals'],
+			$products_filtered['totals'],
+			'products should produce the same totals as product_includes.'
+		);
+		$this->assertNotEquals(
+			$second['product']->get_id(),
+			$first['product']->get_id()
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Variations/Stats/ControllerTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Variations/Stats/ControllerTest.php
new file mode 100644
index 00000000000..cfc3b56367d
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Variations/Stats/ControllerTest.php
@@ -0,0 +1,57 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Variations\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Controller;
+use ReflectionMethod;
+use WC_Unit_Test_Case;
+use WP_REST_Request;
+
+/**
+ * Tests for the Variations Stats report controller query params.
+ */
+class ControllerTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var Controller
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->sut = new Controller();
+	}
+
+	/**
+	 * @testdox get_collection_params registers the products alias used by Analytics single-product views.
+	 */
+	public function test_get_collection_params_registers_products_alias(): void {
+		$params = $this->sut->get_collection_params();
+
+		$this->assertArrayHasKey( 'products', $params, 'Stats should accept the products alias the Variations table sends.' );
+		$this->assertArrayHasKey( 'product_includes', $params, 'Stats should still accept product_includes directly.' );
+		$this->assertSame( 'array', $params['products']['type'] );
+	}
+
+	/**
+	 * @testdox prepare_reports_query maps products to product_includes like the Variations list endpoint.
+	 */
+	public function test_prepare_reports_query_maps_products_to_product_includes(): void {
+		$request = new WP_REST_Request( 'GET', '/wc-analytics/reports/variations/stats' );
+		$request->set_param( 'products', array( 12, 34 ) );
+
+		$method = new ReflectionMethod( Controller::class, 'prepare_reports_query' );
+		$method->setAccessible( true );
+		$query_args = $method->invoke( $this->sut, $request );
+
+		$this->assertArrayHasKey( 'product_includes', $query_args );
+		$this->assertSame( array( 12, 34 ), $query_args['product_includes'] );
+		$this->assertArrayNotHasKey( 'products', $query_args, 'The alias should be mapped, not passed through unchanged.' );
+	}
+}