Commit ac7eeb4d39b for woocommerce

commit ac7eeb4d39b071cc5c43968d094fc1dd585b661e
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date:   Mon Jun 22 13:12:14 2026 +0100

    Check product status when saving items to shopper lists (#65889)

diff --git a/plugins/woocommerce/changelog/fix-shopper-lists-non-published-products b/plugins/woocommerce/changelog/fix-shopper-lists-non-published-products
new file mode 100644
index 00000000000..18735a4b1f0
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-shopper-lists-non-published-products
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Only save published products to shopper lists.
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
index 139a05d8d62..6fe986b5280 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
@@ -130,11 +130,11 @@ class ShopperListItem {
 	 * @param int   $product_or_variation_id Product or variation ID.
 	 * @param array $variation               Variation attributes keyed by attribute name.
 	 * @param int   $quantity                Saved quantity. Coerced to a minimum of 1.
-	 * @return self|null Null if the underlying product can't be resolved.
+	 * @return self|null Null if the underlying product can't be resolved or isn't published.
 	 */
 	public static function from_product( int $product_or_variation_id, array $variation = array(), int $quantity = 1 ): ?self {
 		$product = wc_get_product( absint( $product_or_variation_id ) );
-		if ( ! $product ) {
+		if ( ! $product || ! self::product_is_live( $product ) ) {
 			return null;
 		}

@@ -313,7 +313,16 @@ class ShopperListItem {
 	 */
 	public function is_live(): bool {
 		$product = $this->get_product();
-		if ( ! $product instanceof \WC_Product || ProductStatus::PUBLISH !== $product->get_status() ) {
+		return $product instanceof \WC_Product && self::product_is_live( $product );
+	}
+
+	/**
+	 * Whether a resolved product (and its parent, for variations) is `publish`.
+	 *
+	 * @param \WC_Product $product Resolved product or variation.
+	 */
+	private static function product_is_live( \WC_Product $product ): bool {
+		if ( ProductStatus::PUBLISH !== $product->get_status() ) {
 			return false;
 		}

diff --git a/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListItemTests.php b/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListItemTests.php
index 4f1479dc6e9..54b3da1b4e6 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListItemTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListItemTests.php
@@ -85,6 +85,29 @@ class ShopperListItemTests extends WC_Unit_Test_Case {
 		$this->assertSame( $original->to_array(), $rebuilt->to_array() );
 	}

+	/**
+	 * @testdox from_product should return null for a product that isn't publicly live.
+	 */
+	public function test_from_product_returns_null_for_non_published_product(): void {
+		$this->product->set_status( 'private' );
+		$this->product->save();
+
+		$this->assertNull( ShopperListItem::from_product( $this->product->get_id() ) );
+	}
+
+	/**
+	 * @testdox from_product should return null for a non-published variable product, not a variation error.
+	 */
+	public function test_from_product_returns_null_for_non_published_variable_product(): void {
+		$variable = \WC_Helper_Product::create_variation_product();
+		$variable->set_status( 'draft' );
+		$variable->save();
+
+		$this->assertNull( ShopperListItem::from_product( $variable->get_id() ) );
+
+		$variable->delete( true );
+	}
+
 	/**
 	 * @testdox from_product validates the variation array against the variation product, like cart does.
 	 */