Commit 73fee4f9705 for woocommerce

commit 73fee4f9705bd1d1109bae56abfbc043c5a2a276
Author: Jacob Max Jensen <jmj@webko.dk>
Date:   Fri Mar 20 06:20:18 2026 +0100

    Fix: Attribute terms REST API writes menu_order to wrong meta key (#63390)

    * Fix attribute terms REST API using wrong meta key for menu_order

    * Add unit test for attribute terms menu_order meta key

    * Fix tests, add isset guard, add changelog for menu_order meta key fix

    - Remove garbled test file (space character filename) and add tests to
      the existing V1 controller test class at the correct path.
    - Add isset($request['menu_order']) guard to prevent overwriting order
      meta when updating unrelated term fields (matches categories controller).
    - Add tests for create, update, read, old-key regression, and the
      isset guard preserving existing order.
    - Add missing changelog entry.

    * Fix PHPCS lint issues in attribute terms controller tests

    - Move file docblock before declare(strict_types) per WooCommerce standards
    - Add short descriptions to all test method docblocks
    - Fix equals sign alignment

    ---------

    Co-authored-by: Brandon Kraft <public@brandonkraft.com>

diff --git a/plugins/woocommerce/changelog/fix-attribute-terms-menu-order-meta-key b/plugins/woocommerce/changelog/fix-attribute-terms-menu-order-meta-key
new file mode 100644
index 00000000000..6f4732bcbb2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-attribute-terms-menu-order-meta-key
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix attribute terms REST API writing menu_order to wrong meta key.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php
index 564ba016667..5fdb3f09148 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php
@@ -135,7 +135,7 @@ class WC_REST_Product_Attribute_Terms_V1_Controller extends WC_REST_Terms_Contro
 	 */
 	public function prepare_item_for_response( $item, $request ) {
 		// Get term order.
-		$menu_order = get_term_meta( $item->term_id, 'order_' . $this->taxonomy, true );
+		$menu_order = get_term_meta( $item->term_id, 'order', true );

 		$data = array(
 			'id'          => (int) $item->term_id,
@@ -176,7 +176,9 @@ class WC_REST_Product_Attribute_Terms_V1_Controller extends WC_REST_Terms_Contro
 	protected function update_term_meta_fields( $term, $request ) {
 		$id = (int) $term->term_id;

-		update_term_meta( $id, 'order_' . $this->taxonomy, $request['menu_order'] );
+		if ( isset( $request['menu_order'] ) ) {
+			update_term_meta( $id, 'order', $request['menu_order'] );
+		}

 		return true;
 	}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller-tests.php
index 8dddd3f8008..ee5f26def72 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller-tests.php
@@ -1,4 +1,10 @@
 <?php
+/**
+ * Product attribute terms controller tests for V1 REST API.
+ *
+ * @package WooCommerce\Tests\RestApi
+ */
+
 declare( strict_types = 1 );

 use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
@@ -8,12 +14,14 @@ use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
  */
 class WC_REST_Product_Attribute_Terms_V1_Controller_Tests extends WC_REST_Unit_Test_Case {
 	/**
-	 * @var int Admin user id.
+	 * Admin user ID.
+	 *
+	 * @var int
 	 */
 	private $admin_id;

 	/**
-	 * Test setup.
+	 * Set up test fixtures.
 	 */
 	public function setUp(): void {
 		parent::setUp();
@@ -21,6 +29,8 @@ class WC_REST_Product_Attribute_Terms_V1_Controller_Tests extends WC_REST_Unit_T
 	}

 	/**
+	 * Test that the item schema contains expected properties.
+	 *
 	 * @testdox Product attribute terms item schema contains expected properties.
 	 */
 	public function test_get_item_schema() {
@@ -42,6 +52,8 @@ class WC_REST_Product_Attribute_Terms_V1_Controller_Tests extends WC_REST_Unit_T
 	}

 	/**
+	 * Test that creating a term with an empty slug succeeds.
+	 *
 	 * @testdox Creating a product attribute term with an empty slug succeeds.
 	 */
 	public function test_create_with_empty_slug() {
@@ -61,4 +73,136 @@ class WC_REST_Product_Attribute_Terms_V1_Controller_Tests extends WC_REST_Unit_T

 		$this->assertEquals( 201, $response->get_status() );
 	}
+
+	/**
+	 * Test that creating a term stores menu_order under the correct meta key.
+	 *
+	 * @testdox Creating a term via REST API stores menu_order under the 'order' meta key.
+	 */
+	public function test_menu_order_writes_to_correct_meta_key() {
+		wp_set_current_user( $this->admin_id );
+
+		$attribute_id = wc_create_attribute(
+			array(
+				'name'     => 'Test Size',
+				'slug'     => 'test-size',
+				'order_by' => 'menu_order',
+			)
+		);
+		$taxonomy     = wc_attribute_taxonomy_name( 'test-size' );
+		register_taxonomy( $taxonomy, array( 'product' ) );
+
+		$request = new WP_REST_Request( 'POST', '/wc/v1/products/attributes/' . $attribute_id . '/terms' );
+		$request->set_body_params(
+			array(
+				'name'       => 'Large',
+				'slug'       => 'large',
+				'menu_order' => 5,
+			)
+		);
+		$response = $this->server->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 201, $response->get_status() );
+		$this->assertEquals( 5, $data['menu_order'] );
+		$this->assertEquals( 5, (int) get_term_meta( $data['id'], 'order', true ) );
+		$this->assertEmpty( get_term_meta( $data['id'], 'order_' . $taxonomy, true ), 'Old meta key should not be written' );
+
+		wc_delete_attribute( $attribute_id );
+	}
+
+	/**
+	 * Test that updating menu_order updates the correct meta key.
+	 *
+	 * @testdox Updating menu_order via REST API updates the 'order' meta key.
+	 */
+	public function test_menu_order_update_writes_to_correct_meta_key() {
+		wp_set_current_user( $this->admin_id );
+
+		$attribute_id = wc_create_attribute(
+			array(
+				'name'     => 'Test Weight',
+				'slug'     => 'test-weight',
+				'order_by' => 'menu_order',
+			)
+		);
+		$taxonomy     = wc_attribute_taxonomy_name( 'test-weight' );
+		register_taxonomy( $taxonomy, array( 'product' ) );
+
+		$term = wp_insert_term( 'Medium', $taxonomy, array( 'slug' => 'medium' ) );
+		update_term_meta( $term['term_id'], 'order', 0 );
+
+		$request = new WP_REST_Request( 'PUT', '/wc/v1/products/attributes/' . $attribute_id . '/terms/' . $term['term_id'] );
+		$request->set_body_params( array( 'menu_order' => 3 ) );
+		$response = $this->server->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertEquals( 3, $data['menu_order'] );
+		$this->assertEquals( 3, (int) get_term_meta( $term['term_id'], 'order', true ) );
+
+		wc_delete_attribute( $attribute_id );
+	}
+
+	/**
+	 * Test that reading menu_order returns the value from the correct meta key.
+	 *
+	 * @testdox Reading menu_order via GET returns value from the 'order' meta key.
+	 */
+	public function test_menu_order_read_uses_correct_meta_key() {
+		wp_set_current_user( $this->admin_id );
+
+		$attribute_id = wc_create_attribute(
+			array(
+				'name'     => 'Test Material',
+				'slug'     => 'test-material',
+				'order_by' => 'menu_order',
+			)
+		);
+		$taxonomy     = wc_attribute_taxonomy_name( 'test-material' );
+		register_taxonomy( $taxonomy, array( 'product' ) );
+
+		$term = wp_insert_term( 'Cotton', $taxonomy, array( 'slug' => 'cotton' ) );
+		update_term_meta( $term['term_id'], 'order', 7 );
+
+		$request  = new WP_REST_Request( 'GET', '/wc/v1/products/attributes/' . $attribute_id . '/terms/' . $term['term_id'] );
+		$response = $this->server->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertEquals( 7, $data['menu_order'] );
+
+		wc_delete_attribute( $attribute_id );
+	}
+
+	/**
+	 * Test that updating a term without menu_order preserves existing order.
+	 *
+	 * @testdox Updating a term without menu_order does not overwrite existing order.
+	 */
+	public function test_update_without_menu_order_preserves_existing_order() {
+		wp_set_current_user( $this->admin_id );
+
+		$attribute_id = wc_create_attribute(
+			array(
+				'name'     => 'Test Style',
+				'slug'     => 'test-style',
+				'order_by' => 'menu_order',
+			)
+		);
+		$taxonomy     = wc_attribute_taxonomy_name( 'test-style' );
+		register_taxonomy( $taxonomy, array( 'product' ) );
+
+		$term = wp_insert_term( 'Casual', $taxonomy, array( 'slug' => 'casual' ) );
+		update_term_meta( $term['term_id'], 'order', 5 );
+
+		$request = new WP_REST_Request( 'PUT', '/wc/v1/products/attributes/' . $attribute_id . '/terms/' . $term['term_id'] );
+		$request->set_body_params( array( 'description' => 'Updated description' ) );
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertEquals( 5, (int) get_term_meta( $term['term_id'], 'order', true ), 'Order should be preserved when menu_order is not in the request' );
+
+		wc_delete_attribute( $attribute_id );
+	}
 }