Commit e284581280 for woocommerce

commit e284581280f5ae9bb111bbc35e703cb31603aacc
Author: Abdalsalaam Halawa <abdalsalaamnafez@gmail.com>
Date:   Tue Dec 16 18:41:08 2025 +0400

    Add support for filtering product categories by parent level in the Store API (#62447)

    * Add parent parameter to AbstractTermsRoute for filtering hierarchical terms

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Apply suggested description

    * use built-in is_taxonomy_hierarchical

    * Test term parent

    ---------

    Co-authored-by: github-actions <github-actions@github.com>
    Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/62447-add-issue-62446 b/plugins/woocommerce/changelog/62447-add-issue-62446
new file mode 100644
index 0000000000..3e0a87a806
--- /dev/null
+++ b/plugins/woocommerce/changelog/62447-add-issue-62446
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add support for filtering product categories by parent level in the Store API.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractTermsRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractTermsRoute.php
index 2c9274e9fd..7b7f084fb9 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractTermsRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractTermsRoute.php
@@ -96,6 +96,13 @@ abstract class AbstractTermsRoute extends AbstractRoute {
 			'default'     => true,
 		);

+		$params['parent'] = array(
+			'description'       => __( 'Limit results to terms with a specific parent (hierarchical taxonomies only).', 'woocommerce' ),
+			'type'              => 'integer',
+			'sanitize_callback' => 'absint',
+			'validate_callback' => 'rest_validate_request_arg',
+		);
+
 		return $params;
 	}

@@ -122,6 +129,10 @@ abstract class AbstractTermsRoute extends AbstractRoute {
 			'search'     => $request['search'],
 		);

+		if ( isset( $request['parent'] ) && is_taxonomy_hierarchical( $taxonomy ) ) {
+			$prepared_args['parent'] = (int) $request['parent'];
+		}
+
 		$term_query = new WP_Term_Query();
 		$objects    = $term_query->query( $prepared_args );
 		$return     = [];
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductBrands.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductBrands.php
index 686b5ddbcc..c5b6cedc2c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductBrands.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductBrands.php
@@ -29,6 +29,13 @@ class ProductBrands extends ControllerTestCase {
 			)
 		);

+		$this->child_brand = $fixtures->get_product_brand(
+			array(
+				'name'   => 'Child Brand',
+				'parent' => $this->product_brand['term_id'],
+			)
+		);
+
 		$this->products = array(
 			$fixtures->get_simple_product(
 				array(
@@ -43,6 +50,13 @@ class ProductBrands extends ControllerTestCase {
 					'brand_ids'     => array( $this->product_brand['term_id'] ),
 				)
 			),
+			$fixtures->get_simple_product(
+				array(
+					'name'          => 'Test Product 3',
+					'regular_price' => 50,
+					'brand_ids'     => array( $this->child_brand['term_id'] ),
+				)
+			),
 		);
 	}

@@ -55,7 +69,7 @@ class ProductBrands extends ControllerTestCase {

 		// Assert correct response format.
 		$this->assertSame( 200, $response->get_status(), 'Unexpected status code.' );
-		$this->assertSame( 1, count( $data ), 'Unexpected item count.' );
+		$this->assertSame( 2, count( $data ), 'Unexpected item count.' );

 		// Assert response items contain the correct properties.
 		$this->assertArrayHasKey( 'id', $data[0] );
@@ -66,6 +80,61 @@ class ProductBrands extends ControllerTestCase {
 		$this->assertArrayHasKey( 'count', $data[0] );
 	}

+	/**
+	 * Test getting only top-level brands using parent=0 parameter.
+	 */
+	public function test_get_items_with_parent_zero() {
+		$request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/brands' );
+		$request->set_param( 'parent', 0 );
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertSame( 200, $response->get_status(), 'Unexpected status code.' );
+		$this->assertSame( 1, count( $data ), 'Expected only top-level brands.' );
+		$this->assertSame( 'Test Brand 1', $data[0]['name'] );
+		$this->assertSame( 0, $data[0]['parent'] );
+	}
+
+	/**
+	 * Test getting child brands using parent parameter with parent brand ID.
+	 */
+	public function test_get_items_with_parent_id() {
+		$request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/brands' );
+		$request->set_param( 'parent', $this->product_brand['term_id'] );
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertSame( 200, $response->get_status(), 'Unexpected status code.' );
+		$this->assertSame( 1, count( $data ), 'Expected only child brands of specified parent.' );
+		$this->assertSame( 'Child Brand', $data[0]['name'] );
+		$this->assertSame( $this->product_brand['term_id'], $data[0]['parent'] );
+	}
+
+	/**
+	 * Test that parent parameter with non-existent ID returns empty results.
+	 */
+	public function test_get_items_with_parent_nonexistent() {
+		$request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/brands' );
+		$request->set_param( 'parent', 9 );
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertSame( 200, $response->get_status(), 'Unexpected status code.' );
+		$this->assertSame( 0, count( $data ), 'Expected no brands for non-existent parent.' );
+	}
+
+	/**
+	 * Test that parent parameter is registered in collection params.
+	 */
+	public function test_collection_params_include_parent() {
+		$request  = new \WP_REST_Request( 'OPTIONS', '/wc/store/v1/products/brands' );
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+		$params   = $data['endpoints'][0]['args'];
+
+		$this->assertArrayHasKey( 'parent', $params );
+		$this->assertSame( 'integer', $params['parent']['type'] );
+	}

 	/**
 	 * Test getting brands from a specific product.