Commit 5fa549207c7 for woocommerce

commit 5fa549207c79e22c4015f7c342a79492d57c826e
Author: Deepak Lalwani <deepak.lalwani81@gmail.com>
Date:   Mon Apr 27 05:13:59 2026 +0530

    Add 'product.published' webhook topic (#63555)

    * Add 'product.published' webhook topic

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

    * Enhance product published webhook to include product variations and add tests for transition actions

    * Fix @since tag version and array alignment for product.published webhook

    Update @since from 10.7.0 to 10.8.0 to match current release version.
    Fix double arrow alignment in webhook topic arrays to satisfy phpcs.

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Michael Pretty <prettyboymp@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/63555-enhancement-27650-product-published-webhook-topic b/plugins/woocommerce/changelog/63555-enhancement-27650-product-published-webhook-topic
new file mode 100644
index 00000000000..5e15455dff9
--- /dev/null
+++ b/plugins/woocommerce/changelog/63555-enhancement-27650-product-published-webhook-topic
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Add 'product.published' webhook topic than runs when a product is published.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php b/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php
index 66c3d772f3e..fb61dd1fe15 100644
--- a/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php
+++ b/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php
@@ -65,23 +65,24 @@ if ( ! defined( 'ABSPATH' ) ) {
 							$topics = apply_filters(
 								'woocommerce_webhook_topics',
 								array(
-									''                 => __( 'Select an option&hellip;', 'woocommerce' ),
-									'coupon.created'   => __( 'Coupon created', 'woocommerce' ),
-									'coupon.updated'   => __( 'Coupon updated', 'woocommerce' ),
-									'coupon.deleted'   => __( 'Coupon deleted', 'woocommerce' ),
-									'coupon.restored'  => __( 'Coupon restored', 'woocommerce' ),
-									'customer.created' => __( 'Customer created', 'woocommerce' ),
-									'customer.updated' => __( 'Customer updated', 'woocommerce' ),
-									'customer.deleted' => __( 'Customer deleted', 'woocommerce' ),
-									'order.created'    => __( 'Order created', 'woocommerce' ),
-									'order.updated'    => __( 'Order updated', 'woocommerce' ),
-									'order.deleted'    => __( 'Order deleted', 'woocommerce' ),
-									'order.restored'   => __( 'Order restored', 'woocommerce' ),
-									'product.created'  => __( 'Product created', 'woocommerce' ),
-									'product.updated'  => __( 'Product updated', 'woocommerce' ),
-									'product.deleted'  => __( 'Product deleted', 'woocommerce' ),
-									'product.restored' => __( 'Product restored', 'woocommerce' ),
-									'action'           => __( 'Action', 'woocommerce' ),
+									''                  => __( 'Select an option&hellip;', 'woocommerce' ),
+									'coupon.created'    => __( 'Coupon created', 'woocommerce' ),
+									'coupon.updated'    => __( 'Coupon updated', 'woocommerce' ),
+									'coupon.deleted'    => __( 'Coupon deleted', 'woocommerce' ),
+									'coupon.restored'   => __( 'Coupon restored', 'woocommerce' ),
+									'customer.created'  => __( 'Customer created', 'woocommerce' ),
+									'customer.updated'  => __( 'Customer updated', 'woocommerce' ),
+									'customer.deleted'  => __( 'Customer deleted', 'woocommerce' ),
+									'order.created'     => __( 'Order created', 'woocommerce' ),
+									'order.updated'     => __( 'Order updated', 'woocommerce' ),
+									'order.deleted'     => __( 'Order deleted', 'woocommerce' ),
+									'order.restored'    => __( 'Order restored', 'woocommerce' ),
+									'product.created'   => __( 'Product created', 'woocommerce' ),
+									'product.updated'   => __( 'Product updated', 'woocommerce' ),
+									'product.deleted'   => __( 'Product deleted', 'woocommerce' ),
+									'product.restored'  => __( 'Product restored', 'woocommerce' ),
+									'product.published' => __( 'Product published', 'woocommerce' ),
+									'action'            => __( 'Action', 'woocommerce' ),
 								)
 							);

diff --git a/plugins/woocommerce/includes/class-wc-post-data.php b/plugins/woocommerce/includes/class-wc-post-data.php
index 56fa862a165..3326045eed1 100644
--- a/plugins/woocommerce/includes/class-wc-post-data.php
+++ b/plugins/woocommerce/includes/class-wc-post-data.php
@@ -147,6 +147,16 @@ class WC_Post_Data {
 		if ( ( ProductStatus::PUBLISH === $new_status || ProductStatus::PUBLISH === $old_status ) && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
 			self::delete_product_query_transients();
 		}
+
+		if ( ProductStatus::PUBLISH === $new_status && ProductStatus::PUBLISH !== $old_status && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
+			/**
+			 * Fires when a product or product variation transitions to published status.
+			 *
+			 * @since 10.8.0
+			 * @param int $product_id The product or variation ID.
+			 */
+			do_action( 'woocommerce_product_published', $post->ID );
+		}
 	}

 	/**
diff --git a/plugins/woocommerce/includes/class-wc-webhook.php b/plugins/woocommerce/includes/class-wc-webhook.php
index a0234cd305e..cef7bf36d7f 100644
--- a/plugins/woocommerce/includes/class-wc-webhook.php
+++ b/plugins/woocommerce/includes/class-wc-webhook.php
@@ -941,63 +941,66 @@ class WC_Webhook extends WC_Legacy_Webhook {
 	 */
 	private function get_topic_hooks( $topic ) {
 		$topic_hooks = array(
-			'coupon.created'   => array(
+			'coupon.created'    => array(
 				'woocommerce_process_shop_coupon_meta',
 				'woocommerce_new_coupon',
 			),
-			'coupon.updated'   => array(
+			'coupon.updated'    => array(
 				'woocommerce_process_shop_coupon_meta',
 				'woocommerce_update_coupon',
 			),
-			'coupon.deleted'   => array(
+			'coupon.deleted'    => array(
 				'wp_trash_post',
 			),
-			'coupon.restored'  => array(
+			'coupon.restored'   => array(
 				'untrashed_post',
 			),
-			'customer.created' => array(
+			'customer.created'  => array(
 				'user_register',
 				'woocommerce_created_customer',
 				'woocommerce_new_customer',
 			),
-			'customer.updated' => array(
+			'customer.updated'  => array(
 				'profile_update',
 				'woocommerce_update_customer',
 			),
-			'customer.deleted' => array(
+			'customer.deleted'  => array(
 				'delete_user',
 			),
-			'order.created'    => array(
+			'order.created'     => array(
 				'woocommerce_new_order',
 			),
-			'order.updated'    => array(
+			'order.updated'     => array(
 				'woocommerce_update_order',
 				'woocommerce_order_refunded',
 			),
-			'order.deleted'    => array(
+			'order.deleted'     => array(
 				'wp_trash_post',
 				'woocommerce_trash_order',
 			),
-			'order.restored'   => array(
+			'order.restored'    => array(
 				'untrashed_post',
 				'woocommerce_untrash_order',
 			),
-			'product.created'  => array(
+			'product.created'   => array(
 				'woocommerce_process_product_meta',
 				'woocommerce_new_product',
 				'woocommerce_new_product_variation',
 			),
-			'product.updated'  => array(
+			'product.updated'   => array(
 				'woocommerce_process_product_meta',
 				'woocommerce_update_product',
 				'woocommerce_update_product_variation',
 			),
-			'product.deleted'  => array(
+			'product.deleted'   => array(
 				'wp_trash_post',
 			),
-			'product.restored' => array(
+			'product.restored'  => array(
 				'untrashed_post',
 			),
+			'product.published' => array(
+				'woocommerce_product_published',
+			),
 		);

 		$topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this );
diff --git a/plugins/woocommerce/includes/wc-webhook-functions.php b/plugins/woocommerce/includes/wc-webhook-functions.php
index 62448b8b6c6..d16e16dd658 100644
--- a/plugins/woocommerce/includes/wc-webhook-functions.php
+++ b/plugins/woocommerce/includes/wc-webhook-functions.php
@@ -117,7 +117,13 @@ function wc_is_webhook_valid_topic( $topic ) {
 	}

 	$valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) );
-	$valid_events    = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) );
+	/**
+	 * Filters the list of valid webhook events.
+	 *
+	 * @since 2.2.0
+	 * @param array $valid_events Array of valid webhook events.
+	 */
+	$valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored', 'published' ) );

 	if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) {
 		return true;
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php
index 46294873d65..f51e754d32b 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php
@@ -30,6 +30,7 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
 			array( true, wc_is_webhook_valid_topic( 'product.updated' ) ),
 			array( true, wc_is_webhook_valid_topic( 'product.deleted' ) ),
 			array( true, wc_is_webhook_valid_topic( 'product.restored' ) ),
+			array( true, wc_is_webhook_valid_topic( 'product.published' ) ),
 			array( true, wc_is_webhook_valid_topic( 'order.created' ) ),
 			array( true, wc_is_webhook_valid_topic( 'order.updated' ) ),
 			array( true, wc_is_webhook_valid_topic( 'order.deleted' ) ),
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php b/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php
index c170c643794..3384529bec7 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php
@@ -45,4 +45,121 @@ class WC_Post_Data_Test extends \WC_Unit_Test_Case {
 		$order = wc_get_order( $order->get_id() );
 		$this->assertEmpty( $order->get_items() );
 	}
+
+	/**
+	 * @testdox Should fire woocommerce_product_published when product transitions to publish status.
+	 */
+	public function test_transition_post_status_fires_product_published_action(): void {
+		$product = \WC_Helper_Product::create_simple_product( false );
+		$product->set_status( 'draft' );
+		$product->save();
+
+		$published_ids = array();
+		$callback      = function ( $product_id ) use ( &$published_ids ) {
+			$published_ids[] = $product_id;
+		};
+		add_action( 'woocommerce_product_published', $callback );
+
+		$post = get_post( $product->get_id() );
+		WC_Post_Data::transition_post_status( 'publish', 'draft', $post );
+
+		$this->assertContains( $product->get_id(), $published_ids, 'woocommerce_product_published should fire when product transitions to publish' );
+
+		remove_action( 'woocommerce_product_published', $callback );
+		$product->delete( true );
+	}
+
+	/**
+	 * @testdox Should not fire woocommerce_product_published when product is already published and updated.
+	 */
+	public function test_transition_post_status_does_not_fire_product_published_on_update(): void {
+		$product = \WC_Helper_Product::create_simple_product();
+
+		$published_ids = array();
+		$callback      = function ( $product_id ) use ( &$published_ids ) {
+			$published_ids[] = $product_id;
+		};
+		add_action( 'woocommerce_product_published', $callback );
+
+		$post = get_post( $product->get_id() );
+		WC_Post_Data::transition_post_status( 'publish', 'publish', $post );
+
+		$this->assertEmpty( $published_ids, 'woocommerce_product_published should not fire when product is already published' );
+
+		remove_action( 'woocommerce_product_published', $callback );
+		$product->delete( true );
+	}
+
+	/**
+	 * @testdox Should not fire woocommerce_product_published for non-product post types.
+	 */
+	public function test_transition_post_status_does_not_fire_product_published_for_non_products(): void {
+		$post_id = wp_insert_post(
+			array(
+				'post_title'  => 'Test Post',
+				'post_type'   => 'post',
+				'post_status' => 'draft',
+			)
+		);
+
+		$published_ids = array();
+		$callback      = function ( $product_id ) use ( &$published_ids ) {
+			$published_ids[] = $product_id;
+		};
+		add_action( 'woocommerce_product_published', $callback );
+
+		$post = get_post( $post_id );
+		WC_Post_Data::transition_post_status( 'publish', 'draft', $post );
+
+		$this->assertEmpty( $published_ids, 'woocommerce_product_published should not fire for non-product post types' );
+
+		remove_action( 'woocommerce_product_published', $callback );
+		wp_delete_post( $post_id, true );
+	}
+
+	/**
+	 * @testdox Should fire woocommerce_product_published when a product variation transitions to publish status.
+	 */
+	public function test_transition_post_status_fires_product_published_action_for_variation(): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_status( 'draft' );
+		$variation->save();
+
+		$published_ids = array();
+		$callback      = function ( $product_id ) use ( &$published_ids ) {
+			$published_ids[] = $product_id;
+		};
+		add_action( 'woocommerce_product_published', $callback );
+
+		$post = get_post( $variation->get_id() );
+		WC_Post_Data::transition_post_status( 'publish', 'draft', $post );
+
+		$this->assertContains( $variation->get_id(), $published_ids, 'woocommerce_product_published should fire when a variation transitions to publish' );
+
+		remove_action( 'woocommerce_product_published', $callback );
+		$variation->delete( true );
+	}
+
+	/**
+	 * @testdox Should fire woocommerce_product_published when a scheduled product transitions from future to publish.
+	 */
+	public function test_transition_post_status_fires_product_published_action_on_scheduled_publish(): void {
+		$product = \WC_Helper_Product::create_simple_product( false );
+		$product->set_status( 'future' );
+		$product->save();
+
+		$published_ids = array();
+		$callback      = function ( $product_id ) use ( &$published_ids ) {
+			$published_ids[] = $product_id;
+		};
+		add_action( 'woocommerce_product_published', $callback );
+
+		$post = get_post( $product->get_id() );
+		WC_Post_Data::transition_post_status( 'publish', 'future', $post );
+
+		$this->assertContains( $product->get_id(), $published_ids, 'woocommerce_product_published should fire when a scheduled product transitions from future to publish' );
+
+		remove_action( 'woocommerce_product_published', $callback );
+		$product->delete( true );
+	}
 }