Commit 376e1586c38 for woocommerce

commit 376e1586c38dd3daf90e535c157a4303efb2520d
Author: Neil Carlo Sucuangco <necafasu@gmail.com>
Date:   Tue Mar 3 02:52:24 2026 +0800

    fix: Validate sold individually products in shortcode cart/checkout (#62293)

    * fix: Validate sold individually products in shortcode cart/checkout

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

    * Update class-wc-cart-test.php

    * Update class-wc-cart.php

    * fix: Validate sold individually products in shortcode cart/checkout

    * Update class-wc-cart-test.php

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

    * Update class-wc-cart.php

    * Lint fixes

    * Update class-wc-cart-test.php

    * Update class-wc-cart.php

    * Lint fix

    ---------

    Co-authored-by: github-actions <github-actions@github.com>
    Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/62293-fix-cart-sold-individually-validation b/plugins/woocommerce/changelog/62293-fix-cart-sold-individually-validation
new file mode 100644
index 00000000000..a459a838abb
--- /dev/null
+++ b/plugins/woocommerce/changelog/62293-fix-cart-sold-individually-validation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fixed sold individually validation on shortcode cart and checkout pages. Products marked as "Sold Individually" after being added to cart now have quantity automatically reduced to 1 with correct totals calculation.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index 5610d391290..7df4a6f4d07 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -774,6 +774,15 @@ class WC_Cart extends WC_Legacy_Cart {
 			$return = false;
 		}

+		$result = $this->check_cart_item_sold_individually();
+
+		if ( is_wp_error( $result ) ) {
+			foreach ( $result->get_error_messages() as $message ) {
+				wc_add_notice( $message, 'error' );
+			}
+			$return = false;
+		}
+
 		$result = $this->check_cart_item_stock();

 		if ( is_wp_error( $result ) ) {
@@ -820,6 +829,41 @@ class WC_Cart extends WC_Legacy_Cart {
 		return $return;
 	}

+	/**
+	 * Looks through cart items and ensures sold individually products have quantity of 1.
+	 *
+	 * @since 10.7.0
+	 * @return bool|WP_Error
+	 */
+	public function check_cart_item_sold_individually() {
+		$errors = new WP_Error();
+
+		foreach ( $this->get_cart() as $cart_item_key => $values ) {
+			$product = $values['data'];
+
+			if ( ! $product || ! $product->exists() ) {
+				continue;
+			}
+
+			$product_id       = $values['variation_id'] ? $values['variation_id'] : $values['product_id'];
+			$product_to_check = wc_get_product( $product_id );
+
+			if ( ! $product_to_check || ! $product_to_check->exists() ) {
+				continue;
+			}
+
+			if ( $product_to_check->is_sold_individually() && $values['quantity'] > 1 ) {
+				// Re-fetch and overwrite to reflect product changes made after item was added to cart.
+				$this->cart_contents[ $cart_item_key ]['data'] = $product_to_check;
+				$this->set_quantity( $cart_item_key, 1, false );
+				/* translators: %s: product name */
+				$errors->add( 'sold-individually', sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $product_to_check->get_name() ) );
+			}
+		}
+
+		return $errors->has_errors() ? $errors : true;
+	}
+
 	/**
 	 * Looks through the cart to check each item is in stock. If not, add an error.
 	 *
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index 3a23a7cd40e..635fdb1bba4 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -112,6 +112,71 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 		wp_delete_user( $user_id );
 	}

+	/**
+	 * @testdox check_cart_items should reduce quantity to 1 when product is marked as sold individually after being added to cart
+	 */
+	public function test_check_cart_items_reduces_sold_individually_quantity() {
+		WC()->cart->empty_cart();
+		WC()->session->set( 'wc_notices', null );
+
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_regular_price( 10 );
+		$product->save();
+
+		WC()->cart->add_to_cart( $product->get_id(), 2 );
+
+		$product->set_sold_individually( true );
+		$product->save();
+
+		WC()->session->set( 'wc_notices', null );
+
+		$result = WC()->cart->check_cart_items();
+		$this->assertFalse( $result, 'check_cart_items should return false when fixing sold individually quantity (indicating an issue was found)' );
+
+		$cart_contents_after = WC()->cart->get_cart();
+		$cart_item_after     = array_values( $cart_contents_after )[0];
+		$this->assertEquals( 1, $cart_item_after['quantity'], 'Cart item quantity should be reduced to 1' );
+
+		$error_notices = wp_list_pluck( wc_get_notices( 'error' ), 'notice' );
+		$this->assertContains(
+			sprintf( 'You can only have 1 %s in your cart.', $product->get_name() ),
+			$error_notices
+		);
+
+		WC()->cart->empty_cart();
+		WC()->session->set( 'wc_notices', null );
+		$product->delete( true );
+	}
+
+	/**
+	 * @testdox Sold individually product with quantity 1 should not trigger an error or get modified by check_cart_items
+	 */
+	public function test_check_cart_items_does_not_modify_sold_individually_quantity_one() {
+		WC()->cart->empty_cart();
+		WC()->session->set( 'wc_notices', null );
+
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_regular_price( 10 );
+		$product->set_sold_individually( true );
+		$product->save();
+
+		WC()->cart->add_to_cart( $product->get_id(), 1 );
+
+		$result = WC()->cart->check_cart_items();
+		$this->assertTrue( $result, 'check_cart_items should return true when no issues found' );
+
+		$cart_contents = WC()->cart->get_cart();
+		$cart_item     = array_values( $cart_contents )[0];
+		$this->assertEquals( 1, $cart_item['quantity'], 'Quantity should remain 1' );
+
+		$error_notices = wp_list_pluck( wc_get_notices( 'error' ), 'notice' );
+		$this->assertEmpty( $error_notices, 'No error notices should be added' );
+
+		WC()->cart->empty_cart();
+		WC()->session->set( 'wc_notices', null );
+		$product->delete( true );
+	}
+
 	/**
 	 * @testdox should throw a notice to the cart if an "any" attribute is empty.
 	 */