Commit ca3f5cf67bf for woocommerce

commit ca3f5cf67bf5c04bd6c057a0e7f0dcf09b259747
Author: Thilina Pituwala <thilina.hasantha@gmail.com>
Date:   Fri Oct 9 13:29:38 2026 +1100

    Update/woomkt 991 add utm params to woocommercecom subscription inbox note (#69465)

    * Add UTM params to WooCommerce.com subscription inbox note links

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Add changelog entry for subscription inbox note UTM params

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Use utm_source=pu for subscription inbox note links

    Match the Plugins screen subscription notices, which already use
    utm_source=pu, so both placements report under the same source.

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * Make has_action_url() static

    The helper only reads the note passed to it, so it is pure and the
    backend convention requires it to be static.

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * Test that up-to-date expired notes are not rewritten

    Add a test that fails if has_action_url() stops matching the stored
    Renew Subscription URL, and explain why it compares with esc_url_raw().

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/update-woomkt-991-inbox-subscription-note-utm b/plugins/woocommerce/changelog/update-woomkt-991-inbox-subscription-note-utm
new file mode 100644
index 00000000000..dacd33ec05e
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-woomkt-991-inbox-subscription-note-utm
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Add UTM params to WooCommerce.com subscription Inbox note links; not visible to merchants.
+
diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php b/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php
index 12c56657caf..6160c26c84f 100644
--- a/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php
+++ b/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php
@@ -23,6 +23,7 @@ class WooSubscriptionsNotes {
 	const SUBSCRIPTION_NOTE_NAME  = 'wc-admin-wc-helper-subscription';
 	const NOTIFY_WHEN_DAYS_LEFT   = 60;
 	const BUMP_THRESHOLDS         = array( 60, 45, 20, 7, 1 ); // days.
+	private const UTM_SOURCE      = 'pu';

 	/**
 	 * Hook all the things.
@@ -320,7 +321,13 @@ class WooSubscriptionsNotes {
 		$note->add_action(
 			'enable-autorenew',
 			__( 'Enable Autorenew', 'woocommerce' ),
-			'https://woocommerce.com/my-account/my-subscriptions/?utm_medium=product'
+			add_query_arg(
+				array(
+					'utm_source'   => self::UTM_SOURCE,
+					'utm_campaign' => 'pu_inbox_enable_autorenew',
+				),
+				'https://woocommerce.com/my-account/my-subscriptions/'
+			)
 		);
 		$note->set_content( $note_content );
 		$note->set_content_data( $note_content_data );
@@ -338,11 +345,18 @@ class WooSubscriptionsNotes {
 		$product_page = $subscription['product_url'];
 		$expires      = intval( $subscription['expires'] );
 		$expires_date = gmdate( 'F jS', $expires );
+		$renew_url    = '' === (string) $product_page ? '' : add_query_arg(
+			array(
+				'utm_source'   => self::UTM_SOURCE,
+				'utm_campaign' => 'pu_inbox_renew',
+			),
+			$product_page
+		);

 		$note = $this->find_note_for_product_id( $product_id );
 		if ( $note ) {
 			$note_content_data = $note->get_content_data();
-			if ( $note_content_data->expired ) {
+			if ( $note_content_data->expired && self::has_action_url( $note, 'renew-subscription', $renew_url ) ) {
 				// We've already got a full fledged expired note for this. Bail.
 				// Expired notes' content don't change with time.
 				return;
@@ -383,11 +397,32 @@ class WooSubscriptionsNotes {
 		$note->add_action(
 			'renew-subscription',
 			__( 'Renew Subscription', 'woocommerce' ),
-			$product_page
+			$renew_url
 		);
 		$note->save();
 	}

+	/**
+	 * Whether a note already has the given action with the given URL.
+	 *
+	 * Lets expired notes saved before a link change pick up the new URL on the next refresh.
+	 *
+	 * @param Note   $note        The note to check.
+	 * @param string $action_name The action name.
+	 * @param string $url         The expected action URL.
+	 * @return bool
+	 */
+	private static function has_action_url( Note $note, string $action_name, string $url ): bool {
+		foreach ( (array) $note->get_actions() as $action ) {
+			if ( isset( $action->name, $action->query ) && $action_name === $action->name ) {
+				// Note::add_action() stores the URL through esc_url_raw(), so compare against the same form.
+				return esc_url_raw( $url ) === $action->query;
+			}
+		}
+
+		return false;
+	}
+
 	/**
 	 * For each active subscription on this site, checks the expiration date and creates/updates/deletes notes.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Notes/WooSubscriptionsNotesTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Notes/WooSubscriptionsNotesTest.php
new file mode 100644
index 00000000000..30920a020d8
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Notes/WooSubscriptionsNotesTest.php
@@ -0,0 +1,141 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Admin\Notes;
+
+use Automattic\WooCommerce\Admin\Notes\Note;
+use Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the WooSubscriptionsNotes class.
+ */
+class WooSubscriptionsNotesTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var WooSubscriptionsNotes
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->sut = new WooSubscriptionsNotes();
+	}
+
+	/**
+	 * @testdox Should link the expiring note's Enable Autorenew action to My Subscriptions with inbox UTM params.
+	 */
+	public function test_expiring_note_action_has_inbox_utm_params(): void {
+		$this->sut->add_or_update_subscription_expiring(
+			array(
+				'product_id'   => 101,
+				'product_name' => 'Test Extension',
+				'expires'      => time() + 30 * DAY_IN_SECONDS,
+			)
+		);
+
+		$action = $this->get_note_action( 101, 'enable-autorenew' );
+
+		$this->assertSame(
+			'https://woocommerce.com/my-account/my-subscriptions/?utm_source=pu&utm_campaign=pu_inbox_enable_autorenew',
+			$action->query,
+			'Enable Autorenew should link to My Subscriptions with the inbox UTM params'
+		);
+	}
+
+	/**
+	 * @testdox Should link the expired note's Renew Subscription action to the product page with inbox UTM params.
+	 */
+	public function test_expired_note_action_has_inbox_utm_params(): void {
+		$this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 102 ) );
+
+		$action = $this->get_note_action( 102, 'renew-subscription' );
+
+		$this->assertSame(
+			'https://woocommerce.com/products/test-extension/?utm_source=pu&utm_campaign=pu_inbox_renew',
+			$action->query,
+			'Renew Subscription should link to the product page with the inbox UTM params'
+		);
+	}
+
+	/**
+	 * @testdox Should update the action URL of an expired note saved before the UTM params were added.
+	 */
+	public function test_existing_expired_note_gets_new_action_url(): void {
+		$this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 103 ) );
+		$note = $this->sut->find_note_for_product_id( 103 );
+		$note->clear_actions();
+		$note->add_action( 'renew-subscription', 'Renew Subscription', 'https://woocommerce.com/products/test-extension/' );
+		$note->save();
+
+		$this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 103 ) );
+
+		$action = $this->get_note_action( 103, 'renew-subscription' );
+		$this->assertSame(
+			'https://woocommerce.com/products/test-extension/?utm_source=pu&utm_campaign=pu_inbox_renew',
+			$action->query,
+			'A stale expired note should pick up the new action URL'
+		);
+	}
+
+	/**
+	 * @testdox Should not rewrite an expired note whose Renew Subscription URL is already current.
+	 */
+	public function test_up_to_date_expired_note_is_not_rewritten(): void {
+		$this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 105 ) );
+		$action_id = $this->get_note_action( 105, 'renew-subscription' )->id;
+
+		$this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 105 ) );
+
+		$this->assertSame( $action_id, $this->get_note_action( 105, 'renew-subscription' )->id, 'An expired note with the current URL should not be rewritten' );
+	}
+
+	/**
+	 * @testdox Should leave the Renew Subscription URL empty when the subscription has no product page.
+	 */
+	public function test_expired_note_without_product_page_has_empty_action_url(): void {
+		$subscription                = $this->get_expired_subscription( 104 );
+		$subscription['product_url'] = '';
+
+		$this->sut->add_or_update_subscription_expired( $subscription );
+
+		$this->assertSame( '', $this->get_note_action( 104, 'renew-subscription' )->query, 'UTM params should not be added to an empty URL' );
+	}
+
+	/**
+	 * Builds an expired subscription record as returned by the WooCommerce.com API.
+	 *
+	 * @param int $product_id The product ID.
+	 * @return array
+	 */
+	private function get_expired_subscription( int $product_id ): array {
+		return array(
+			'product_id'   => $product_id,
+			'product_name' => 'Test Extension',
+			'product_url'  => 'https://woocommerce.com/products/test-extension/',
+			'expires'      => time() - 10 * DAY_IN_SECONDS,
+		);
+	}
+
+	/**
+	 * Loads the saved subscription note for a product and returns one of its actions.
+	 *
+	 * @param int    $product_id  The product ID.
+	 * @param string $action_name The action name.
+	 * @return object
+	 */
+	private function get_note_action( int $product_id, string $action_name ): object {
+		$note = $this->sut->find_note_for_product_id( $product_id );
+		$this->assertInstanceOf( Note::class, $note, 'A subscription note should exist for the product' );
+
+		$actions = wp_list_filter( $note->get_actions(), array( 'name' => $action_name ) );
+		$this->assertCount( 1, $actions, "The note should have one {$action_name} action" );
+
+		return reset( $actions );
+	}
+}