Commit 86f11e2027 for woocommerce

commit 86f11e202717b852a027d5201c283471b42e7528
Author: Neil Carlo Sucuangco <necafasu@gmail.com>
Date:   Tue Jan 20 21:07:40 2026 +0800

    Fix: Order endpoint returns empty values for 'Any' variation attributes (#62476)

    * Fix: Order endpoint returns empty values for 'Any' variation attributes

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

    * Update OrderItemSchema.php

    * Update OrderItemSchema.php

    * yoda conditional to satisfy lint

    ---------

    Co-authored-by: github-actions <github-actions@github.com>
    Co-authored-by: Mike Jolley <mike.jolley@me.com>

diff --git a/plugins/woocommerce/changelog/62476-fix-order-variation-attributes-any b/plugins/woocommerce/changelog/62476-fix-order-variation-attributes-any
new file mode 100644
index 0000000000..7b2e5de5b5
--- /dev/null
+++ b/plugins/woocommerce/changelog/62476-fix-order-variation-attributes-any
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Store API order endpoint returning empty values for "Any" variation attributes. Retrieves customer selections from order metadata to match cart endpoint behavior.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
index 03cef4899b..9941817d05 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
@@ -16,6 +16,13 @@ class OrderItemSchema extends ItemSchema {
 	 */
 	protected $title = 'order_item';

+	/**
+	 * Cache for parent product attributes.
+	 *
+	 * @var array|null
+	 */
+	private $cached_parent_attributes = null;
+
 	/**
 	 * The schema item identifier.
 	 *
@@ -30,6 +37,8 @@ class OrderItemSchema extends ItemSchema {
 	 * @return array
 	 */
 	public function get_item_response( $order_item ) {
+		$this->cached_parent_attributes = null;
+
 		$order   = $order_item->get_order();
 		$product = $order_item->get_product();

@@ -72,10 +81,11 @@ class OrderItemSchema extends ItemSchema {
 			$product_properties['prices']             = $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) );
 			$product_properties['sold_individually']  = $product->is_sold_individually();
 			$product_properties['images']             = $this->get_images( $product );
+
 			// Only include variation data for product variations, not simple products.
 			// This is consistent with the cart endpoint behavior.
 			if ( $product instanceof \WC_Product_Variation ) {
-				$product_properties['variation'] = $this->format_variation_data( $product->get_attributes(), $product );
+				$product_properties['variation'] = $this->get_variation_data_from_order_item( $order_item, $product );
 			}
 		}

@@ -121,4 +131,85 @@ class OrderItemSchema extends ItemSchema {
 			'line_total_tax'    => $this->prepare_money_response( $order_item->get_total_tax(), wc_get_price_decimals() ),
 		];
 	}
+
+	/**
+	 * Get variation data from order item metadata.
+	 *
+	 * Gets the customer's actual attribute choices from the order metadata.
+	 * This fixes variations set to "Any" returning empty values.
+	 *
+	 * @param \WC_Order_Item_Product $order_item Order item instance.
+	 * @param \WC_Product            $product Product instance.
+	 * @return array Formatted variation data.
+	 */
+	protected function get_variation_data_from_order_item( $order_item, $product ) {
+		$variation_data = array();
+		$meta_data      = $order_item->get_meta_data();
+
+		$parent_attributes = $this->get_parent_product_attributes( $product );
+
+		foreach ( $meta_data as $meta ) {
+			$meta_key   = $meta->key;
+			$meta_value = $meta->value;
+
+			if ( empty( $meta_key ) || ! is_scalar( $meta_value ) || '' === $meta_value || strpos( $meta_key, '_' ) === 0 ) {
+				continue;
+			}
+
+			$is_variation_attribute = false;
+			$normalized_key         = $meta_key;
+
+			if ( strpos( $meta_key, 'attribute_' ) === 0 ) {
+				$normalized_key = substr( $meta_key, strlen( 'attribute_' ) );
+			}
+
+			if ( strpos( $normalized_key, 'pa_' ) === 0 ) {
+				$is_variation_attribute = true;
+			} else {
+				foreach ( $parent_attributes as $attribute ) {
+					if ( strtolower( $attribute->get_name() ) === strtolower( $normalized_key ) ) {
+						$is_variation_attribute = true;
+						break;
+					}
+				}
+			}
+
+			if ( $is_variation_attribute ) {
+				$variation_data[ wc_variation_attribute_name( $normalized_key ) ] = $meta_value;
+			}
+		}
+
+		return $this->format_variation_data( $variation_data, $product );
+	}
+
+	/**
+	 * Get parent product attributes.
+	 *
+	 * Cached to avoid multiple DB lookups when processing multiple meta items.
+	 *
+	 * @param \WC_Product $product Product instance.
+	 * @return array Array of WC_Product_Attribute objects.
+	 *
+	 * @since 10.5.0
+	 */
+	protected function get_parent_product_attributes( $product ) {
+		if ( null !== $this->cached_parent_attributes ) {
+			return $this->cached_parent_attributes;
+		}
+
+		$this->cached_parent_attributes = array();
+
+		if ( ! $product->get_parent_id() ) {
+			return $this->cached_parent_attributes;
+		}
+
+		$parent_product = wc_get_product( $product->get_parent_id() );
+		if ( ! $parent_product instanceof \WC_Product ) {
+			return $this->cached_parent_attributes;
+		}
+
+		$this->cached_parent_attributes = $parent_product->get_attributes();
+
+		return $this->cached_parent_attributes;
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
index 21bd272146..0d88bad611 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
@@ -240,4 +240,181 @@ class OrderItemSchemaTest extends TestCase {
 		$order->delete( true );
 		$variable_product->delete( true );
 	}
+
+	/**
+	 * Test that variations with "Any" attributes return user-selected values.
+	 */
+	public function test_get_item_response_any_variation_returns_user_selected_values(): void {
+		$variable_product = new \WC_Product_Variable();
+		$variable_product->set_name( 'Test Variable Product' );
+
+		$attribute = new \WC_Product_Attribute();
+		$attribute->set_id( 0 );
+		$attribute->set_name( 'pa_size' );
+		$attribute->set_options( array( 'small', 'medium', 'large' ) );
+		$attribute->set_visible( true );
+		$attribute->set_variation( true );
+
+		$variable_product->set_attributes( array( $attribute ) );
+		$variable_product->save();
+
+		$variation = new \WC_Product_Variation();
+		$variation->set_parent_id( $variable_product->get_id() );
+		$variation->set_attributes( array( 'pa_size' => '' ) );
+		$variation->set_regular_price( '10.00' );
+		$variation->save();
+
+		$order = new \WC_Order();
+		$item  = new \WC_Order_Item_Product();
+		$item->set_props(
+			array(
+				'product_id'   => $variable_product->get_id(),
+				'variation_id' => $variation->get_id(),
+				'quantity'     => 1,
+				'subtotal'     => '10.00',
+				'total'        => '10.00',
+				'name'         => $variation->get_name(),
+			)
+		);
+
+		$item->add_meta_data( 'pa_size', 'large', true );
+		$order->add_item( $item );
+		$order->save();
+
+		$result = $this->sut->get_item_response( $item );
+
+		$this->assertArrayHasKey( 'variation', $result );
+		$this->assertIsArray( $result['variation'] );
+		$this->assertNotEmpty( $result['variation'] );
+		$this->assertCount( 1, $result['variation'] );
+
+		$variation_data = $result['variation'][0];
+		$this->assertArrayHasKey( 'raw_attribute', $variation_data );
+		$this->assertArrayHasKey( 'attribute', $variation_data );
+		$this->assertArrayHasKey( 'value', $variation_data );
+		$this->assertEquals( 'attribute_pa_size', $variation_data['raw_attribute'] );
+		$this->assertEquals( 'size', $variation_data['attribute'] );
+		$this->assertEquals( 'large', $variation_data['value'] );
+
+		$order->delete( true );
+		$variation->delete( true );
+		$variable_product->delete( true );
+	}
+
+	/**
+	 * Test variations with mixed global and custom "Any" attributes.
+	 */
+	public function test_get_item_response_any_variation_with_mixed_attributes(): void {
+		$variable_product = new \WC_Product_Variable();
+		$variable_product->set_name( 'Test Variable Product with Mixed Attributes' );
+
+		$global_attribute = new \WC_Product_Attribute();
+		$global_attribute->set_id( 0 );
+		$global_attribute->set_name( 'pa_size' );
+		$global_attribute->set_options( array( 'small', 'medium', 'large' ) );
+		$global_attribute->set_visible( true );
+		$global_attribute->set_variation( true );
+
+		$custom_attribute = new \WC_Product_Attribute();
+		$custom_attribute->set_id( 0 );
+		$custom_attribute->set_name( 'color' );
+		$custom_attribute->set_options( array( 'red', 'blue', 'green' ) );
+		$custom_attribute->set_visible( true );
+		$custom_attribute->set_variation( true );
+
+		$variable_product->set_attributes( array( $global_attribute, $custom_attribute ) );
+		$variable_product->save();
+
+		$variation = new \WC_Product_Variation();
+		$variation->set_parent_id( $variable_product->get_id() );
+		$variation->set_attributes(
+			array(
+				'pa_size' => '',
+				'color'   => '',
+			)
+		);
+		$variation->set_regular_price( '15.00' );
+		$variation->save();
+
+		$order = new \WC_Order();
+		$item  = new \WC_Order_Item_Product();
+		$item->set_props(
+			array(
+				'product_id'   => $variable_product->get_id(),
+				'variation_id' => $variation->get_id(),
+				'quantity'     => 1,
+				'subtotal'     => '15.00',
+				'total'        => '15.00',
+				'name'         => $variation->get_name(),
+			)
+		);
+
+		$item->add_meta_data( 'pa_size', 'large', true );
+		$item->add_meta_data( 'color', 'blue', true );
+		$order->add_item( $item );
+		$order->save();
+
+		$result = $this->sut->get_item_response( $item );
+
+		$this->assertArrayHasKey( 'variation', $result );
+		$this->assertIsArray( $result['variation'] );
+		$this->assertCount( 2, $result['variation'] );
+
+		$global_attr = null;
+		$custom_attr = null;
+		foreach ( $result['variation'] as $attr ) {
+			if ( 'attribute_pa_size' === $attr['raw_attribute'] ) {
+				$global_attr = $attr;
+			} elseif ( 'attribute_color' === $attr['raw_attribute'] ) {
+				$custom_attr = $attr;
+			}
+		}
+
+		$this->assertNotNull( $global_attr );
+		$this->assertEquals( 'attribute_pa_size', $global_attr['raw_attribute'] );
+		$this->assertEquals( 'size', $global_attr['attribute'] );
+		$this->assertEquals( 'large', $global_attr['value'] );
+
+		$this->assertNotNull( $custom_attr );
+		$this->assertEquals( 'attribute_color', $custom_attr['raw_attribute'] );
+		$this->assertEquals( 'color', $custom_attr['attribute'] );
+		$this->assertEquals( 'blue', $custom_attr['value'] );
+
+		$order->delete( true );
+		$variation->delete( true );
+		$variable_product->delete( true );
+	}
+
+	/**
+	 * Test that specific (non-Any) variations still work correctly.
+	 */
+	public function test_get_item_response_specific_variation_returns_correct_values(): void {
+		$variable_product = \WC_Helper_Product::create_variation_product();
+		$variations       = $variable_product->get_children();
+		$variation        = wc_get_product( $variations[0] );
+
+		$order = new \WC_Order();
+		$order->add_product( $variation, 1 );
+		$order->save();
+
+		$items = $order->get_items();
+		$item  = reset( $items );
+
+		$result = $this->sut->get_item_response( $item );
+
+		$this->assertArrayHasKey( 'variation', $result );
+		$this->assertIsArray( $result['variation'] );
+		$this->assertNotEmpty( $result['variation'] );
+
+		foreach ( $result['variation'] as $attr ) {
+			$this->assertArrayHasKey( 'raw_attribute', $attr );
+			$this->assertArrayHasKey( 'attribute', $attr );
+			$this->assertArrayHasKey( 'value', $attr );
+			$this->assertStringStartsWith( 'attribute_', $attr['raw_attribute'] );
+			$this->assertNotEmpty( $attr['value'] );
+		}
+
+		$order->delete( true );
+		$variable_product->delete( true );
+	}
 }