Commit 0ada82c55e1 for woocommerce
commit 0ada82c55e135d692575062ece6598e327fcd347
Author: Cvetan Cvetanov <cvetan.cvetanov@automattic.com>
Date: Wed Aug 5 16:51:13 2026 +0300
Preserve variation IDs in partial REST order updates (#67343)
* Fix partial REST updates clearing variation IDs
* Add changelog entry for REST variation preservation
* Fix REST variation round-trip edge cases
* Honor SKU resolution in REST order item updates
* Honor authoritative SKU aliases in REST order updates
* Harden REST variation identity matching
* Cover malformed parent SKUs in REST v1
* Fix zero SKU fallback for order item creation
* Narrow REST variation preservation to same-parent updates
* Harden REST variation preservation against filtered IDs
* Harden REST variation update boundaries
* Fix explicit REST variation demotion metadata
* Remove unused REST test binding
diff --git a/plugins/woocommerce/changelog/67119-preserve-rest-variation b/plugins/woocommerce/changelog/67119-preserve-rest-variation
new file mode 100644
index 00000000000..ae740dc9478
--- /dev/null
+++ b/plugins/woocommerce/changelog/67119-preserve-rest-variation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Preserve variation IDs when partially updating order line items through the REST API.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php
index 5b09651f462..2fc39a5d6e0 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php
@@ -670,11 +670,35 @@ class WC_REST_Orders_V1_Controller extends WC_REST_Posts_Controller {
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_line_items( $posted, $action = 'create' ) {
- $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' );
- $product = wc_get_product( $this->get_product_id( $posted, $action ) );
+ $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' );
+ $product = wc_get_product( $this->get_product_id( $posted, $action ) );
+ $current_product_id = (int) $item->get_product_id( 'edit' );
+ $current_variation_id = (int) $item->get_variation_id( 'edit' );
+ // set_product() clears a variation when given its parent. REST partial updates restore it only
+ // when the posted parent and SKU-resolved product still identify the current item.
+ // An explicit variation_id of 0 still demotes the item.
+ $same_product_update = 'update' === $action
+ && array_key_exists( 'product_id', $posted )
+ && $product instanceof WC_Product
+ && (int) $posted['product_id'] === $current_product_id
+ && in_array( $product->get_id(), array( $current_product_id, $current_variation_id ), true );
+ $restore_variation_id = $same_product_update
+ && $current_variation_id
+ && ( ! array_key_exists( 'variation_id', $posted ) || (int) $posted['variation_id'] === $current_variation_id );
+ $clear_variation_id = $same_product_update
+ && $current_variation_id
+ && array_key_exists( 'variation_id', $posted )
+ && 0 === (int) $posted['variation_id'];
+
+ if ( $clear_variation_id && $product instanceof WC_Product_Variation ) {
+ $product = wc_get_product( $current_product_id );
+ }
if ( $product && $product !== $item->get_product() ) {
$item->set_product( $product );
+ if ( $restore_variation_id ) {
+ $item->set_variation_id( $current_variation_id );
+ }
if ( 'create' === $action ) {
$quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1;
@@ -683,6 +707,9 @@ class WC_REST_Orders_V1_Controller extends WC_REST_Posts_Controller {
$item->set_subtotal( $total );
}
}
+ if ( $clear_variation_id ) {
+ $item->set_variation_id( 0 );
+ }
$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
index 3ceb00eed27..325725df428 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
@@ -938,11 +938,36 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_line_items( $posted, $action = 'create', $item = null ) {
- $item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;
- $product = wc_get_product( $this->get_product_id( $posted, $action ) );
+ $item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;
+ $product = wc_get_product( $this->get_product_id( $posted, $action ) );
+ $product_item = $item instanceof WC_Order_Item_Product ? $item : null;
+ $current_product_id = $product_item ? (int) $product_item->get_product_id( 'edit' ) : 0;
+ $current_variation_id = $product_item ? (int) $product_item->get_variation_id( 'edit' ) : 0;
+ // set_product() clears a variation when given its parent. REST partial updates restore it only
+ // when the posted parent and SKU-resolved product still identify the current item.
+ // An explicit variation_id of 0 still demotes the item.
+ $same_product_update = 'update' === $action
+ && array_key_exists( 'product_id', $posted )
+ && $product instanceof WC_Product
+ && (int) $posted['product_id'] === $current_product_id
+ && in_array( $product->get_id(), array( $current_product_id, $current_variation_id ), true );
+ $restore_variation_id = $same_product_update
+ && $current_variation_id
+ && ( ! array_key_exists( 'variation_id', $posted ) || (int) $posted['variation_id'] === $current_variation_id );
+ $clear_variation_id = $same_product_update
+ && $current_variation_id
+ && array_key_exists( 'variation_id', $posted )
+ && 0 === (int) $posted['variation_id'];
+
+ if ( $clear_variation_id && $product instanceof WC_Product_Variation ) {
+ $product = wc_get_product( $current_product_id );
+ }
if ( $product && $product !== $item->get_product() ) {
$item->set_product( $product );
+ if ( $restore_variation_id && $product_item ) {
+ $product_item->set_variation_id( $current_variation_id );
+ }
if ( 'create' === $action ) {
$quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1;
@@ -951,6 +976,9 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
$item->set_subtotal( $total );
}
}
+ if ( $clear_variation_id && $product_item ) {
+ $product_item->set_variation_id( 0 );
+ }
$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );
$this->maybe_set_item_meta_data( $item, $posted );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller-tests.php
index 2777bc4c582..baf0041defd 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller-tests.php
@@ -48,6 +48,25 @@ class WC_REST_Orders_V1_Controller_Tests extends WC_REST_Unit_Test_Case {
);
}
+ /**
+ * Creates an order containing a variation line item.
+ *
+ * @return array{WC_Product_Variable, WC_Product_Variation, WC_Order, WC_Order_Item_Product}
+ */
+ private function create_order_with_variation_line_item(): array {
+ $parent = WC_Helper_Product::create_variation_product();
+ $variation = wc_get_product( $parent->get_children()[0] );
+ $order = new WC_Order();
+ $item = new WC_Order_Item_Product();
+ $item->set_product( $variation );
+ $item->set_quantity( 1 );
+ $item->set_total( 10 );
+ $order->add_item( $item );
+ $order->save();
+
+ return array( $parent, $variation, $order, $item );
+ }
+
/**
* Test that an order can be fetched via REST API V1 without triggering a deprecation notice.
*
@@ -122,4 +141,118 @@ class WC_REST_Orders_V1_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->assertEquals( 400, $response->get_status(), 'The order was not updated, as the specified customer does not belong to the blog.' );
$this->assertEquals( 'woocommerce_rest_invalid_customer_id', $response->get_data()['code'], 'The returned error indicates the customer ID was invalid.' );
}
+
+ /**
+ * @testdox Updating with the unchanged parent preserves the variation while ignoring view-context product ID filters.
+ */
+ public function test_update_line_item_with_unchanged_parent_preserves_variation_id(): void {
+ list( $parent, $variation, $order, $item ) = $this->create_order_with_variation_line_item();
+ $parent->set_name( 'Updated parent name' );
+ $parent->set_tax_class( 'reduced-rate' );
+ $parent->save();
+ $item->set_name( 'Historical line item name' );
+ $item->set_tax_class( '' );
+ $item->save();
+ $filtered_product = WC_Helper_Product::create_simple_product();
+ $product_id_filter = static function ( $product_id, $order_item ) use ( $filtered_product, $item ) {
+ return $item->get_id() === $order_item->get_id() ? $filtered_product->get_id() : $product_id;
+ };
+ $product_id_hook_name = 'woocommerce_order_item_get_product_id';
+ add_filter( $product_id_hook_name, $product_id_filter, 10, 2 );
+
+ $request = new WP_REST_Request( 'PUT', '/wc/v1/orders/' . $order->get_id() );
+ $request->set_body_params(
+ array(
+ 'line_items' => array(
+ array(
+ 'id' => $item->get_id(),
+ 'product_id' => $parent->get_id(),
+ ),
+ ),
+ )
+ );
+
+ try {
+ $response = $this->server->dispatch( $request );
+ } finally {
+ remove_filter( $product_id_hook_name, $product_id_filter, 10 );
+ }
+ $this->assertSame( 200, $response->get_status(), 'The partial update should succeed.' );
+
+ $reloaded = new WC_Order_Item_Product( $item->get_id() );
+ $this->assertSame( $variation->get_id(), $reloaded->get_variation_id(), 'The existing variation ID should be preserved.' );
+ $this->assertSame( $variation->get_id(), $reloaded->get_product()->get_id(), 'The line item should continue to resolve to the variation.' );
+ $this->assertSame( $parent->get_name(), $reloaded->get_name(), 'The line-item name should retain its pre-regression resynchronization behavior.' );
+ $this->assertSame( $parent->get_tax_class(), $reloaded->get_tax_class(), 'The line-item tax class should retain its pre-regression resynchronization behavior.' );
+ }
+
+ /**
+ * @testdox Updating with an explicit zero variation ID clears the variation even when SKU resolves to it.
+ */
+ public function test_update_line_item_with_zero_variation_id_and_current_sku_switches_to_parent(): void {
+ list( $parent, $variation, $order, $item ) = $this->create_order_with_variation_line_item();
+
+ $parent->set_name( 'REST V1 parent product' );
+ $parent->set_tax_class( '' );
+ $parent->save();
+ $variation_sku = 'REST-V1-VARIATION-' . wp_generate_uuid4();
+ $variation->set_sku( $variation_sku );
+ $variation->set_tax_class( 'reduced-rate' );
+ $variation->save();
+ $item->set_name( $variation->get_name() );
+ $item->set_tax_class( $variation->get_tax_class() );
+ $item->save();
+
+ $request = new WP_REST_Request( 'PUT', '/wc/v1/orders/' . $order->get_id() );
+ $request->set_body_params(
+ array(
+ 'line_items' => array(
+ array(
+ 'id' => $item->get_id(),
+ 'product_id' => $parent->get_id(),
+ 'variation_id' => 0,
+ 'sku' => $variation_sku,
+ ),
+ ),
+ )
+ );
+
+ $response = $this->server->dispatch( $request );
+ $this->assertSame( 200, $response->get_status(), 'Explicitly demoting the line item should succeed.' );
+
+ $reloaded = new WC_Order_Item_Product( $item->get_id() );
+ $this->assertSame( 0, $reloaded->get_variation_id(), 'An explicit zero variation ID should clear the existing variation.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product_id(), 'The line item should retain the submitted parent product ID.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product()->get_id(), 'The line item should resolve to the parent product.' );
+ $this->assertSame( $parent->get_name(), $reloaded->get_name(), 'The line-item name should be synchronized with the parent product.' );
+ $this->assertSame( $parent->get_tax_class(), $reloaded->get_tax_class(), 'The line-item tax class should be synchronized with the parent product.' );
+ }
+
+ /**
+ * @testdox Updating with the parent product as variation_id does not restore the existing variation.
+ */
+ public function test_update_line_item_with_parent_as_variation_id_does_not_restore_variation(): void {
+ list( $parent, , $order, $item ) = $this->create_order_with_variation_line_item();
+
+ $request = new WP_REST_Request( 'PUT', '/wc/v1/orders/' . $order->get_id() );
+ $request->set_body_params(
+ array(
+ 'line_items' => array(
+ array(
+ 'id' => $item->get_id(),
+ 'product_id' => $parent->get_id(),
+ 'variation_id' => $parent->get_id(),
+ ),
+ ),
+ )
+ );
+
+ $response = $this->server->dispatch( $request );
+ $this->assertSame( 200, $response->get_status(), 'The update should succeed.' );
+
+ $reloaded = new WC_Order_Item_Product( $item->get_id() );
+ $this->assertSame( 0, $reloaded->get_variation_id(), 'A parent product ID should not be restored as a variation.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product_id(), 'The line item should retain the parent product ID.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product()->get_id(), 'The line item should resolve to the parent product.' );
+ }
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
index d7c5e16fe21..61c54b86f90 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
@@ -1179,6 +1179,21 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
return array( $order, $item->get_id() );
}
+ /**
+ * Dispatches a v3 line-item update.
+ *
+ * @param int $order_id Order ID.
+ * @param array $line_item Line-item payload.
+ * @return WP_REST_Response
+ */
+ private function dispatch_line_item_update( int $order_id, array $line_item ): WP_REST_Response {
+ $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order_id );
+ $request->set_header( 'content-type', 'application/json' );
+ $request->set_body( wp_json_encode( array( 'line_items' => array( $line_item ) ) ) );
+
+ return $this->server->dispatch( $request );
+ }
+
/**
* @testdox PUT /orders that switches a variation line item to a simple product clears variation_id over the REST round trip.
*/
@@ -1190,22 +1205,13 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->assertSame( $variation->get_id(), (int) wc_get_order_item_meta( $item_id, '_variation_id' ), 'Precondition: the line item stored the variation ID.' );
- $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
- $request->set_header( 'content-type', 'application/json' );
- $request->set_body(
- wp_json_encode(
- array(
- 'line_items' => array(
- array(
- 'id' => $item_id,
- 'product_id' => $simple->get_id(),
- ),
- ),
- )
+ $response = $this->dispatch_line_item_update(
+ $order->get_id(),
+ array(
+ 'id' => $item_id,
+ 'product_id' => $simple->get_id(),
)
);
-
- $response = $this->server->dispatch( $request );
$this->assertSame( 200, $response->get_status(), 'The update should succeed.' );
$reloaded = new WC_Order_Item_Product( $item_id );
@@ -1215,32 +1221,183 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
}
/**
- * @testdox PUT /orders with a product_id-only payload targeting the variable parent demotes the item and clears variation_id.
+ * @testdox PUT /orders with an unchanged parent preserves the variation while ignoring view-context variation ID filters.
*/
- public function test_update_line_item_to_variable_parent_clears_variation_id(): void {
+ public function test_update_line_item_with_unchanged_parent_preserves_variation_id(): void {
list( $parent, $variation ) = $this->create_variable_product_with_color_variation();
- list( $order, $item_id ) = $this->create_order_with_variation_line_item( $variation );
-
- $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
- $request->set_header( 'content-type', 'application/json' );
- $request->set_body(
- wp_json_encode(
+ $parent->set_name( 'Updated parent name' );
+ $parent->set_tax_class( 'reduced-rate' );
+ $parent->save();
+ list( $order, $item_id ) = $this->create_order_with_variation_line_item( $variation );
+ $item = new WC_Order_Item_Product( $item_id );
+ $item->set_name( 'Historical line item name' );
+ $item->set_tax_class( '' );
+ $item->save();
+ $variation_id_filter = static function ( $variation_id, $order_item ) use ( $item_id ) {
+ return $item_id === $order_item->get_id() ? 0 : $variation_id;
+ };
+ $variation_id_hook_name = 'woocommerce_order_item_get_variation_id';
+ add_filter( $variation_id_hook_name, $variation_id_filter, 10, 2 );
+
+ try {
+ $response = $this->dispatch_line_item_update(
+ $order->get_id(),
array(
- 'line_items' => array(
- array(
- 'id' => $item_id,
- 'product_id' => $parent->get_id(),
- ),
- ),
+ 'id' => $item_id,
+ 'product_id' => $parent->get_id(),
)
+ );
+ } finally {
+ remove_filter( $variation_id_hook_name, $variation_id_filter, 10 );
+ }
+ $this->assertSame( 200, $response->get_status(), 'A product_id-only payload targeting the variable parent succeeds with no error.' );
+
+ $response_item = $response->get_data()['line_items'][0];
+ $reloaded = new WC_Order_Item_Product( $item_id );
+ $this->assertSame( $variation->get_id(), $reloaded->get_variation_id(), 'Omitting variation_id should preserve the existing variation.' );
+ $this->assertSame( $variation->get_id(), (int) wc_get_order_item_meta( $item_id, '_variation_id' ), 'The persisted _variation_id meta should remain unchanged.' );
+ $this->assertSame( $variation->get_id(), $reloaded->get_product()->get_id(), 'get_product() should continue to resolve to the variation.' );
+ $this->assertSame( $parent->get_id(), $response_item['product_id'], 'The response should retain the variable parent product ID.' );
+ $this->assertSame( $variation->get_id(), $response_item['variation_id'], 'The response should retain the variation ID.' );
+ $this->assertSame( $parent->get_name(), $reloaded->get_name(), 'The line-item name should retain its pre-regression resynchronization behavior.' );
+ $this->assertSame( $parent->get_tax_class(), $reloaded->get_tax_class(), 'The line-item tax class should retain its pre-regression resynchronization behavior.' );
+ }
+
+ /**
+ * @testdox PUT /orders with an explicit zero variation ID demotes a variation even when SKU resolves to it.
+ */
+ public function test_update_line_item_with_zero_variation_id_and_current_sku_switches_to_parent(): void {
+ list( $parent, $variation ) = $this->create_variable_product_with_color_variation();
+
+ $parent->set_name( 'REST V3 parent product' );
+ $parent->set_tax_class( '' );
+ $parent->save();
+ $variation_sku = 'REST-V3-VARIATION-' . wp_generate_uuid4();
+ $variation->set_sku( $variation_sku );
+ $variation->set_tax_class( 'reduced-rate' );
+ $variation->save();
+
+ list( $order, $item_id ) = $this->create_order_with_variation_line_item( $variation );
+
+ $response = $this->dispatch_line_item_update(
+ $order->get_id(),
+ array(
+ 'id' => $item_id,
+ 'product_id' => $parent->get_id(),
+ 'variation_id' => 0,
+ 'sku' => $variation_sku,
)
);
+ $this->assertSame( 200, $response->get_status(), 'Explicitly demoting the line item should succeed.' );
- $response = $this->server->dispatch( $request );
- $this->assertSame( 200, $response->get_status(), 'A product_id-only payload targeting the variable parent succeeds with no error.' );
+ $reloaded = new WC_Order_Item_Product( $item_id );
+ $this->assertSame( 0, $reloaded->get_variation_id(), 'An explicit zero variation ID should clear the existing variation.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product_id(), 'The line item should retain the submitted parent product ID.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product()->get_id(), 'The line item should resolve to the parent product.' );
+ $this->assertSame( $parent->get_name(), $reloaded->get_name(), 'The line-item name should be synchronized with the parent product.' );
+ $this->assertSame( $parent->get_tax_class(), $reloaded->get_tax_class(), 'The line-item tax class should be synchronized with the parent product.' );
+ }
+
+ /**
+ * @testdox PUT /orders with the parent product as variation_id does not restore the existing variation.
+ */
+ public function test_update_line_item_with_parent_as_variation_id_does_not_restore_variation(): void {
+ list( $parent, $variation ) = $this->create_variable_product_with_color_variation();
+ list( $order, $item_id ) = $this->create_order_with_variation_line_item( $variation );
+
+ $response = $this->dispatch_line_item_update(
+ $order->get_id(),
+ array(
+ 'id' => $item_id,
+ 'product_id' => $parent->get_id(),
+ 'variation_id' => $parent->get_id(),
+ )
+ );
+ $this->assertSame( 200, $response->get_status(), 'The update should succeed.' );
+
+ $reloaded = new WC_Order_Item_Product( $item_id );
+ $this->assertSame( 0, $reloaded->get_variation_id(), 'A parent product ID should not be restored as a variation.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product_id(), 'The line item should retain the parent product ID.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product()->get_id(), 'The line item should resolve to the parent product.' );
+ }
+
+ /**
+ * @testdox Round-tripping a variation with its inherited parent SKU preserves the variation ID.
+ */
+ public function test_round_trip_line_item_with_inherited_parent_sku_preserves_variation_id(): void {
+ list( $parent, $variation ) = $this->create_variable_product_with_color_variation();
+ $parent_sku = 'REST-V3-PARENT-' . wp_generate_uuid4();
+ $parent->set_sku( $parent_sku );
+ $parent->save();
+ $variation->set_sku( '' );
+ $variation->save();
+ list( $order, $item_id ) = $this->create_order_with_variation_line_item( $variation );
+
+ $get_request = new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() );
+ $get_response = $this->server->dispatch( $get_request );
+ $this->assertSame( 200, $get_response->get_status(), 'Fetching the order should succeed.' );
+
+ $round_trip_item = $get_response->get_data()['line_items'][0];
+ $this->assertSame( $parent_sku, $round_trip_item['sku'], 'The response should expose the SKU inherited from the parent.' );
+
+ $put_response = $this->dispatch_line_item_update( $order->get_id(), $round_trip_item );
+ $this->assertSame( 200, $put_response->get_status(), 'Round-tripping the line item should succeed.' );
+
+ $reloaded = new WC_Order_Item_Product( $item_id );
+ $this->assertSame( $variation->get_id(), $reloaded->get_variation_id(), 'The inherited parent SKU should not demote the variation.' );
+ $this->assertSame( $variation->get_id(), $reloaded->get_product()->get_id(), 'The line item should continue to resolve to the variation.' );
+ }
+
+ /**
+ * @testdox PUT /orders does not preserve the variation when product_id conflicts with a SKU resolving to the current parent.
+ */
+ public function test_update_line_item_with_different_product_id_and_current_parent_sku_does_not_preserve_variation(): void {
+ list( $parent, $variation ) = $this->create_variable_product_with_color_variation();
+ $parent_sku = 'REST-V3-PARENT-' . wp_generate_uuid4();
+ $parent->set_sku( $parent_sku );
+ $parent->save();
+ list( $order, $item_id ) = $this->create_order_with_variation_line_item( $variation );
+ $different_product = WC_Helper_Product::create_simple_product();
+
+ $response = $this->dispatch_line_item_update(
+ $order->get_id(),
+ array(
+ 'id' => $item_id,
+ 'product_id' => $different_product->get_id(),
+ 'sku' => $parent_sku,
+ )
+ );
+ $this->assertSame( 200, $response->get_status(), 'The SKU-selected product update should succeed.' );
+
+ $reloaded = new WC_Order_Item_Product( $item_id );
+ $this->assertSame( 0, $reloaded->get_variation_id(), 'A conflicting product ID should prevent restoring the variation.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product_id(), 'The line item should retain the product selected by SKU.' );
+ $this->assertSame( $parent->get_id(), $reloaded->get_product()->get_id(), 'The line item should resolve to the product selected by SKU.' );
+ }
+
+ /**
+ * @testdox PUT /orders still switches products when SKU conflicts with the unchanged parent product_id.
+ */
+ public function test_update_line_item_with_different_product_sku_switches_products(): void {
+ list( $parent, $variation ) = $this->create_variable_product_with_color_variation();
+ list( $order, $item_id ) = $this->create_order_with_variation_line_item( $variation );
+ $simple = WC_Helper_Product::create_simple_product();
+ $simple_sku = 'REST-V3-SIMPLE-' . wp_generate_uuid4();
+ $simple->set_sku( $simple_sku );
+ $simple->save();
+
+ $response = $this->dispatch_line_item_update(
+ $order->get_id(),
+ array(
+ 'id' => $item_id,
+ 'product_id' => $parent->get_id(),
+ 'sku' => $simple_sku,
+ )
+ );
+ $this->assertSame( 200, $response->get_status(), 'Switching by SKU should succeed.' );
$reloaded = new WC_Order_Item_Product( $item_id );
- $this->assertSame( 0, $reloaded->get_variation_id(), 'Omitting variation_id demotes the item to its parent and clears variation_id.' );
- $this->assertSame( $parent->get_id(), $reloaded->get_product()->get_id(), 'get_product() should resolve to the variable parent.' );
+ $this->assertSame( 0, $reloaded->get_variation_id(), 'Switching to a simple product by SKU should clear variation_id.' );
+ $this->assertSame( $simple->get_id(), $reloaded->get_product()->get_id(), 'The line item should resolve to the product selected by SKU.' );
}
}