Commit 4f557da6c9e for woocommerce

commit 4f557da6c9eec7ee48cefae5e293ed53bca66b7f
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Thu Oct 15 18:41:25 2026 +0300

    Show complete variation details for back-in-stock sign-ups in admin, My Account, and emails (#69502)

    * fix: show all variation attributes for back-in-stock sign-ups

    * fix: keep fixed variation attributes and avoid repeated attribute in titles

    * fix: keep the variation name in the back-in-stock email title and drop the stale PHPStan baseline entry

    The email title keeps `get_name()` so it matches the `{product_name}` subject placeholder and the existing e2e expectation; only the admin list and My Account switch to the parent title since they render the attribute list next to it. The `\WC_Product` docblock fix made the baseline's "invalid type" entry for email_product_title() obsolete, so remove it.

    * refactor: rename get_variation_attributes_with_posted to get_variation_attributes_with_posted_data

    * fix: show variation attributes below the product title in the stock notifications list

diff --git a/plugins/woocommerce/changelog/fix-wooplug-7915-bis-variation-labels b/plugins/woocommerce/changelog/fix-wooplug-7915-bis-variation-labels
new file mode 100644
index 00000000000..61354cbabb4
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7915-bis-variation-labels
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show all variation details for back-in-stock sign-ups in the admin list, My Account, and emails.
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index d25b6ff9f67..0c8825a46f4 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -1462,6 +1462,11 @@ body.woocommerce_page_wc-customer-stock-notifications {
 	.wrap {
 		margin-inline-start: 0;
 	}
+
+	// Show the variation attributes on their own line below the product title.
+	.column-product .description {
+		display: block;
+	}
 }

 body.post-type-shop_order,
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 4d1c34cf5fb..51e7dbd2a93 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -65340,12 +65340,6 @@ parameters:
 			count: 1
 			path: src/Internal/StockNotifications/Admin/ListTable.php

-		-
-			message: '#^Call to an undefined method Automattic\\WooCommerce\\Internal\\StockNotifications\\Admin\\ListTable\:\:get_product_formatted_variation_list\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Internal/StockNotifications/Admin/ListTable.php
-
 		-
 			message: '#^Call to an undefined method WC_Data_Store\:\:query\(\)\.$#'
 			identifier: method.notFound
@@ -65844,12 +65838,6 @@ parameters:
 			count: 1
 			path: src/Internal/StockNotifications/Emails/EmailTemplatesController.php

-		-
-			message: '#^Call to method get_name\(\) on an unknown class Automattic\\WooCommerce\\Internal\\StockNotifications\\Emails\\WC_Product\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Internal/StockNotifications/Emails/EmailTemplatesController.php
-
 		-
 			message: '#^Call to method get_price_html\(\) on an unknown class Automattic\\WooCommerce\\Internal\\StockNotifications\\Emails\\WC_Product\.$#'
 			identifier: class.notFound
@@ -65916,12 +65904,6 @@ parameters:
 			count: 1
 			path: src/Internal/StockNotifications/Emails/EmailTemplatesController.php

-		-
-			message: '#^Parameter \$product of method Automattic\\WooCommerce\\Internal\\StockNotifications\\Emails\\EmailTemplatesController\:\:email_product_title\(\) has invalid type Automattic\\WooCommerce\\Internal\\StockNotifications\\Emails\\WC_Product\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Internal/StockNotifications/Emails/EmailTemplatesController.php
-
 		-
 			message: '#^Parameter \#1 \$price of method WC_Product\:\:set_price\(\) expects string, int given\.$#'
 			identifier: argument.type
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Admin/ListTable.php b/plugins/woocommerce/src/Internal/StockNotifications/Admin/ListTable.php
index 870fca366c7..7c616190d17 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Admin/ListTable.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Admin/ListTable.php
@@ -205,8 +205,8 @@ class ListTable extends \WP_List_Table {
 			return;
 		}

-		$name                     = $product->get_name();
-		$formatted_variation_list = $this->get_product_formatted_variation_list( true );
+		$name                     = $product->get_title();
+		$formatted_variation_list = $notification->get_product_formatted_variation_list( true );

 		if ( $formatted_variation_list ) {
 			/* translators: product name, identifier */
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailTemplatesController.php b/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailTemplatesController.php
index e577a641d0d..fe18b1ffed9 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailTemplatesController.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailTemplatesController.php
@@ -63,7 +63,7 @@ class EmailTemplatesController {
 	/**
 	 * Email product title.
 	 *
-	 * @param WC_Product   $product The product object.
+	 * @param \WC_Product  $product The product object.
 	 * @param Notification $notification The notification object.
 	 * @param bool         $plain_text Whether the email is plain text.
 	 */
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Notification.php b/plugins/woocommerce/src/Internal/StockNotifications/Notification.php
index 31bbdd3c90f..6b93164eb8c 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Notification.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Notification.php
@@ -389,16 +389,11 @@ class Notification extends \WC_Data {
 	public function get_product_formatted_variation_list( bool $flat = false ) {

 		$product = $this->get_product();
-		if ( ! $product || ! $product->is_type( array( 'variation' ) ) ) {
+		if ( ! $product instanceof \WC_Product_Variation ) {
 			return '';
 		}

-		// Replace list with custom data.
-		$attributes = $this->get_meta( 'posted_attributes' );
-		if ( ! $attributes ) {
-			$attributes = $product->get_attributes();
-		}
-
+		$attributes = $this->get_variation_attributes_with_posted_data( $product );
 		if ( empty( $attributes ) ) {
 			return '';
 		}
@@ -436,12 +431,33 @@ class Notification extends \WC_Data {
 			return (string) $product->get_permalink();
 		}

-		// Posted attributes hold only the values chosen for "Any" attributes, so merge them over the variation's own attributes to build a complete link.
-		// Keys that no longer belong to the variation (e.g. a removed attribute) are dropped so they don't leak into the URL.
+		return (string) $product->get_permalink( array( 'variation' => $this->get_variation_attributes_with_posted_data( $product ) ) );
+	}
+
+	/**
+	 * Get the variation's attributes, with the shopper's posted values filled in.
+	 *
+	 * Posted values only fill the variation's "Any" attributes (empty values); the variation's fixed values always win.
+	 * Keys that no longer belong to the variation (e.g. a removed attribute) are dropped.
+	 *
+	 * @param \WC_Product_Variation $product The notification's variation.
+	 * @return array
+	 */
+	private function get_variation_attributes_with_posted_data( \WC_Product_Variation $product ): array {
 		$variation_attributes = $product->get_variation_attributes();
-		$attributes           = array_merge( $variation_attributes, array_intersect_key( $posted_attributes, $variation_attributes ) );
+		$posted_attributes    = $this->get_meta( 'posted_attributes' );
+		if ( empty( $posted_attributes ) || ! is_array( $posted_attributes ) ) {
+			return $variation_attributes;
+		}
+
+		$any_attributes = array_filter(
+			$variation_attributes,
+			static function ( $value ) {
+				return '' === $value;
+			}
+		);

-		return (string) $product->get_permalink( array( 'variation' => $attributes ) );
+		return array_merge( $variation_attributes, array_intersect_key( $posted_attributes, $any_attributes ) );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
index cd986de2bf0..1eac24435a2 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
@@ -94,6 +94,57 @@ class NotificationTests extends \WC_Unit_Test_Case {
 		$this->assertEquals( '<dl class="variation"><dt>size:</dt><dd>small</dd><dt>colour:</dt><dd>red</dd></dl>', $formatted_variation_attributes );
 	}

+	/**
+	 * @testdox Should list the variation's own attributes alongside the posted "Any" attributes.
+	 */
+	public function test_get_product_formatted_variation_list_merges_posted_attributes(): void {
+		$variable_product = \WC_Helper_Product::create_variation_product();
+		// The first variation only sets "size"; "colour" is left as "Any".
+		$variation_id = $variable_product->get_children()[0];
+
+		$notification = new Notification();
+		$notification->set_product_id( $variation_id );
+		$notification->set_user_email( 'test@example.com' );
+		$notification->save();
+
+		$this->assertSame( 'size: small', $notification->get_product_formatted_variation_list( true ), 'Without posted attributes only the fixed attributes should be listed' );
+
+		// Posted attributes only hold the "Any" values; "attribute_pa_stale" is not a variation attribute and must be dropped.
+		$notification->update_meta_data(
+			'posted_attributes',
+			array(
+				'attribute_pa_colour' => 'red',
+				'attribute_pa_stale'  => 'gone',
+			)
+		);
+		$notification->save();
+
+		$this->assertSame( 'size: small, colour: red', $notification->get_product_formatted_variation_list( true ), 'The fixed attribute should be kept and the posted "Any" attribute added' );
+	}
+
+	/**
+	 * @testdox Should keep the variation's fixed attribute value when a different value was posted for it.
+	 */
+	public function test_get_product_formatted_variation_list_keeps_fixed_attribute_over_posted(): void {
+		$variable_product = \WC_Helper_Product::create_variation_product();
+		// The first variation fixes "size" to "small"; "colour" is left as "Any".
+		$variation_id = $variable_product->get_children()[0];
+
+		$notification = new Notification();
+		$notification->set_product_id( $variation_id );
+		$notification->set_user_email( 'test@example.com' );
+		$notification->update_meta_data(
+			'posted_attributes',
+			array(
+				'attribute_pa_size'   => 'large',
+				'attribute_pa_colour' => 'red',
+			)
+		);
+		$notification->save();
+
+		$this->assertSame( 'size: small, colour: red', $notification->get_product_formatted_variation_list( true ), 'The fixed attribute should win over the posted value, while the "Any" attribute is filled in' );
+	}
+
 	/**
 	 * Test the get_product_permalink method.
 	 */