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