Commit 8a55371668 for woocommerce

commit 8a55371668c269f9f332632b4350c713440aea89
Author: Néstor Soriano <konamiman@konamiman.com>
Date:   Tue Dec 9 15:25:53 2025 +0100

    Fix: order being incorrectly changed to "Completed" (#62191)

diff --git a/plugins/woocommerce/changelog/pr-62191 b/plugins/woocommerce/changelog/pr-62191
new file mode 100644
index 0000000000..232b04bc74
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-62191
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix: order being incorrectly changed to Completed
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index 7312a7e9f4..742949f2bb 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -10,6 +10,7 @@
  * @package     WooCommerce\Classes
  */

+use Automattic\WooCommerce\Caches\OrderCache;
 use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Enums\ProductTaxStatus;
 use Automattic\WooCommerce\Enums\ProductType;
@@ -303,6 +304,14 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {

 		if ( $items_changed ) {
 			delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' );
+
+			// Invalidate the order cache to prevent stale item data.
+			// This fixes a race condition where get_items() may have been called
+			// before items were saved, caching empty items arrays.
+			// See https://github.com/woocommerce/woocommerce/issues/62173.
+			if ( OrderUtil::orders_cache_usage_is_enabled() ) {
+				wc_get_container()->get( OrderCache::class )->remove( $this->get_id() );
+			}
 		}
 	}

diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index 6473f1735f..a951bac96e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -3926,4 +3926,63 @@ class OrdersTableDataStoreTests extends \HposTestCase {
 		$sync->process_batch( array( $order_id ) );
 		$this->assertEquals( false, (bool) $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM {$this->sut::get_orders_table_name()} WHERE id = %d", $order_id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 	}
+
+	/**
+	 * @testdox needs_processing returns correct result when COGS filter triggers get_items() during order hydration.
+	 *
+	 * Reproduces GitHub issue #62173: woocommerce_new_order fires before items are in DB.
+	 * If a hook loads the order and triggers get_items() (e.g., via COGS filter calling get_data()),
+	 * empty items get cached. The fix invalidates the cache when items are saved.
+	 */
+	public function test_needs_processing_with_cogs_filter_triggering_get_items() {
+		$this->toggle_cot_feature_and_usage( true );
+		$this->enable_cogs_feature();
+
+		$this->assertTrue(
+			OrderUtil::orders_cache_usage_is_enabled(),
+			'Order cache must be enabled for this test.'
+		);
+
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_virtual( false );
+		$product->set_downloadable( false );
+		$product->save();
+
+		// COGS filter that calls get_data() (which calls get_items()), priming empty items.
+		$cogs_filter = function ( $cogs_value, $order ) {
+			$order->get_data();
+			return $cogs_value;
+		};
+		add_filter( 'woocommerce_load_order_cogs_value', $cogs_filter, 10, 2 );
+
+		// Load order during woocommerce_new_order (before items are in DB), caching it with empty items.
+		$new_order_hook = function ( $order_id ) {
+			wc_get_order( $order_id );
+		};
+		add_action( 'woocommerce_new_order', $new_order_hook, 10, 1 );
+
+		// Create order with item. woocommerce_new_order fires before save_items().
+		$order = new WC_Order();
+		$item  = new WC_Order_Item_Product();
+		$item->set_props(
+			array(
+				'product'  => $product,
+				'quantity' => 1,
+				'subtotal' => 10,
+				'total'    => 10,
+			)
+		);
+		$order->add_item( $item );
+		$order->save();
+		$order_id = $order->get_id();
+
+		remove_filter( 'woocommerce_load_order_cogs_value', $cogs_filter, 10 );
+		remove_action( 'woocommerce_new_order', $new_order_hook, 10 );
+
+		// Without fix: returns cached order with empty items. With fix: fresh data from DB.
+		$fresh_order = wc_get_order( $order_id );
+
+		$this->assertCount( 1, $fresh_order->get_items(), 'Order should have items from DB.' );
+		$this->assertTrue( $fresh_order->needs_processing(), 'Order with physical product should need processing.' );
+	}
 }