Commit cb7e66911c9 for woocommerce

commit cb7e66911c95ace937f6e124ee9a055ab229ec7c
Author: Hannah Tinkler <hannah.tinkler@gmail.com>
Date:   Wed Mar 4 15:48:11 2026 +0000

    Adds notification trigger classes and tests. (#63456)

    * Adds notification trigger classes and tests.

diff --git a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
index 247375c8618..ae82878c75d 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
@@ -10,6 +10,8 @@ use Automattic\Jetpack\Connection\Manager as JetpackConnectionManager;
 use Automattic\WooCommerce\Internal\PushNotifications\Controllers\PushTokenRestController;
 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
 use Automattic\WooCommerce\Internal\PushNotifications\Services\PendingNotificationStore;
+use Automattic\WooCommerce\Internal\PushNotifications\Triggers\NewOrderNotificationTrigger;
+use Automattic\WooCommerce\Internal\PushNotifications\Triggers\NewReviewNotificationTrigger;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
 use Automattic\WooCommerce\Utilities\FeaturesUtil;
 use WC_Logger;
@@ -71,7 +73,12 @@ class PushNotifications {
 		$this->register_post_types();

 		wc_get_container()->get( PushTokenRestController::class )->register();
-		wc_get_container()->get( PendingNotificationStore::class )->register();
+
+		$store = wc_get_container()->get( PendingNotificationStore::class );
+		$store->register();
+
+		( new NewOrderNotificationTrigger( $store ) )->register();
+		( new NewReviewNotificationTrigger( $store ) )->register();
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Triggers/NewOrderNotificationTrigger.php b/plugins/woocommerce/src/Internal/PushNotifications/Triggers/NewOrderNotificationTrigger.php
new file mode 100644
index 00000000000..d93bd7ab90c
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Triggers/NewOrderNotificationTrigger.php
@@ -0,0 +1,123 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\PushNotifications\Triggers;
+
+use Automattic\WooCommerce\Internal\PushNotifications\Notifications\NewOrderNotification;
+use Automattic\WooCommerce\Internal\PushNotifications\Services\PendingNotificationStore;
+use WC_Order;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Listens for new and status-changed orders and feeds notifications into
+ * the PendingNotificationStore.
+ *
+ * @since 10.7.0
+ */
+class NewOrderNotificationTrigger {
+	/**
+	 * Order statuses that should trigger a notification.
+	 */
+	const NOTIFIABLE_STATUSES = array(
+		/**
+		 * Source: WooCommerce plugin.
+		 */
+		'processing',
+		'on-hold',
+		'completed',
+		/**
+		 * Source: WooCommerce Pre-Orders plugin.
+		 */
+		'pre-order',
+		/**
+		 *  Source: WPCOM - "commonly used custom pre-order status".
+		 */
+		'pre-ordered',
+		/**
+		 *  Source: WooCommerce Deposits plugin.
+		 */
+		'partial-payment',
+	);
+
+	/**
+	 * The pending notification store.
+	 *
+	 * @var PendingNotificationStore
+	 */
+	private PendingNotificationStore $pending_notification_store;
+
+	/**
+	 * Constructs the trigger.
+	 *
+	 * @param PendingNotificationStore $pending_notification_store The notification store.
+	 *
+	 * @since 10.7.0
+	 */
+	public function __construct( PendingNotificationStore $pending_notification_store ) {
+		$this->pending_notification_store = $pending_notification_store;
+	}
+
+	/**
+	 * Registers WordPress hooks for order events.
+	 *
+	 * @return void
+	 *
+	 * @since 10.7.0
+	 */
+	public function register(): void {
+		add_action( 'woocommerce_new_order', array( $this, 'on_new_order' ), 10, 2 );
+		add_action( 'woocommerce_order_status_changed', array( $this, 'on_order_status_changed' ), 10, 4 );
+	}
+
+	/**
+	 * Handles the woocommerce_new_order hook.
+	 *
+	 * @param int      $order_id The order ID.
+	 * @param WC_Order $order    The order object.
+	 * @return void
+	 *
+	 * @since 10.7.0
+	 */
+	public function on_new_order( int $order_id, WC_Order $order ): void {
+		if ( ! in_array( $order->get_status(), self::NOTIFIABLE_STATUSES, true ) ) {
+			return;
+		}
+
+		$this->pending_notification_store->add(
+			new NewOrderNotification( $order_id )
+		);
+	}
+
+	// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+	/**
+	 * Handles the woocommerce_order_status_changed hook.
+	 *
+	 * @param int      $order_id        The order ID.
+	 * @param string   $previous_status The previous order status.
+	 * @param string   $next_status     The new order status.
+	 * @param WC_Order $order           The order object.
+	 * @return void
+	 *
+	 * @since 10.7.0
+	 */
+	public function on_order_status_changed(
+		int $order_id,
+		string $previous_status,
+		string $next_status,
+		WC_Order $order
+	): void {
+		// phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+		if (
+			in_array( $previous_status, self::NOTIFIABLE_STATUSES, true )
+			|| ! in_array( $next_status, self::NOTIFIABLE_STATUSES, true )
+		) {
+			return;
+		}
+
+		$this->pending_notification_store->add(
+			new NewOrderNotification( $order_id )
+		);
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Triggers/NewReviewNotificationTrigger.php b/plugins/woocommerce/src/Internal/PushNotifications/Triggers/NewReviewNotificationTrigger.php
new file mode 100644
index 00000000000..306d12b82e9
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Triggers/NewReviewNotificationTrigger.php
@@ -0,0 +1,78 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\PushNotifications\Triggers;
+
+use Automattic\WooCommerce\Internal\PushNotifications\Notifications\NewReviewNotification;
+use Automattic\WooCommerce\Internal\PushNotifications\Services\PendingNotificationStore;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Listens for new approved product reviews and feeds notifications into
+ * the PendingNotificationStore.
+ *
+ * @since 10.7.0
+ */
+class NewReviewNotificationTrigger {
+	/**
+	 * The pending notification store.
+	 *
+	 * @var PendingNotificationStore
+	 */
+	private PendingNotificationStore $store;
+
+	/**
+	 * Constructs the trigger.
+	 *
+	 * @param PendingNotificationStore $store The notification store.
+	 *
+	 * @since 10.7.0
+	 */
+	public function __construct( PendingNotificationStore $store ) {
+		$this->store = $store;
+	}
+
+	/**
+	 * Registers WordPress hooks for review events.
+	 *
+	 * @return void
+	 *
+	 * @since 10.7.0
+	 */
+	public function register(): void {
+		add_action( 'comment_post', array( $this, 'on_comment_post' ), 10, 3 );
+	}
+
+	/**
+	 * Handles the comment_post hook.
+	 *
+	 * Only creates a notification for non-spam reviews on products.
+	 *
+	 * @param int        $comment_id       The comment ID.
+	 * @param int|string $comment_approved 1 if approved, 0 if not, 'spam' if spam.
+	 * @param array      $commentdata      The comment data.
+	 * @return void
+	 *
+	 * @since 10.7.0
+	 */
+	public function on_comment_post( int $comment_id, $comment_approved, array $commentdata ): void {
+		if (
+			'spam' === $comment_approved
+			|| 'review' !== ( $commentdata['comment_type'] ?? '' )
+		) {
+			return;
+		}
+
+		$commented_on = get_post_type( (int) ( $commentdata['comment_post_ID'] ?? 0 ) );
+
+		if ( 'product' !== $commented_on ) {
+			return;
+		}
+
+		$this->store->add(
+			new NewReviewNotification( $comment_id )
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Triggers/NewOrderNotificationTriggerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Triggers/NewOrderNotificationTriggerTest.php
new file mode 100644
index 00000000000..500913ad099
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Triggers/NewOrderNotificationTriggerTest.php
@@ -0,0 +1,143 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Triggers;
+
+use Automattic\WooCommerce\Internal\PushNotifications\Services\PendingNotificationStore;
+use Automattic\WooCommerce\Internal\PushNotifications\Triggers\NewOrderNotificationTrigger;
+use WC_Order;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the NewOrderNotificationTrigger class.
+ */
+class NewOrderNotificationTriggerTest extends WC_Unit_Test_Case {
+	/**
+	 * An instance of NewOrderNotificationTrigger.
+	 *
+	 * @var NewOrderNotificationTrigger
+	 */
+	private $trigger;
+
+	/**
+	 * The notification store used by the trigger.
+	 *
+	 * @var PendingNotificationStore
+	 */
+	private $store;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->store = new PendingNotificationStore();
+		$this->store->register();
+
+		wc_get_container()->replace( PendingNotificationStore::class, $this->store );
+		wc_get_container()->reset_all_resolved();
+
+		$this->trigger = new NewOrderNotificationTrigger( $this->store );
+		$this->trigger->register();
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		remove_action( 'woocommerce_new_order', array( $this->trigger, 'on_new_order' ) );
+		remove_action( 'woocommerce_order_status_changed', array( $this->trigger, 'on_order_status_changed' ) );
+		remove_action( 'shutdown', array( $this->store, 'dispatch_all' ) );
+
+		$this->reset_container_replacements();
+		wc_get_container()->reset_all_resolved();
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Should add a notification when a new order is created with a notifiable status.
+	 */
+	public function test_new_order_with_notifiable_status_adds_notification(): void {
+		wc_create_order( array( 'status' => 'processing' ) );
+
+		$this->assertSame( 1, $this->store->count(), 'Exactly one notification should be stored even though both hooks fire' );
+	}
+
+	/**
+	 * @testdox Should not add a notification when a new order is created with a non-notifiable status.
+	 */
+	public function test_new_order_with_non_notifiable_status_is_ignored(): void {
+		wc_create_order( array( 'status' => 'pending' ) );
+
+		$this->assertSame( 0, $this->store->count() );
+	}
+
+	/**
+	 * @testdox Should add a notification when an order status changes to a notifiable status.
+	 */
+	public function test_status_change_to_notifiable_adds_notification(): void {
+		$order = wc_create_order( array( 'status' => 'pending' ) );
+		$this->assertSame( 0, $this->store->count() );
+
+		$order->set_status( 'processing' );
+		$order->save();
+
+		$this->assertSame( 1, $this->store->count() );
+	}
+
+	/**
+	 * @testdox Should accept all notifiable statuses.
+	 * @dataProvider notifiable_statuses_provider
+	 *
+	 * @param string $status The order status.
+	 */
+	public function test_all_notifiable_statuses_accepted( string $status ): void {
+		$order = $this->createMock( WC_Order::class );
+		$order->method( 'get_status' )->willReturn( $status );
+
+		$this->trigger->on_new_order( 1, $order );
+
+		$this->assertSame( 1, $this->store->count(), "Status '$status' should be notifiable" );
+	}
+
+	/**
+	 * @testdox Should not add a notification when order status changes between two notifiable statuses.
+	 */
+	public function test_status_change_between_notifiable_statuses_is_ignored(): void {
+		$order = $this->createMock( WC_Order::class );
+
+		$this->trigger->on_order_status_changed( 1, 'processing', 'completed', $order );
+
+		$this->assertSame( 0, $this->store->count() );
+	}
+
+	/**
+	 * @testdox Should not add a notification when order status changes to a non-notifiable status.
+	 */
+	public function test_on_order_status_changed_ignores_non_notifiable_status(): void {
+		$order = $this->createMock( WC_Order::class );
+
+		$this->trigger->on_order_status_changed( 1, 'pending', 'cancelled', $order );
+
+		$this->assertSame( 0, $this->store->count() );
+	}
+
+	/**
+	 * Data provider for all notifiable statuses.
+	 *
+	 * @return array<string, array{string}>
+	 */
+	public function notifiable_statuses_provider(): array {
+		return array(
+			'processing'      => array( 'processing' ),
+			'on-hold'         => array( 'on-hold' ),
+			'completed'       => array( 'completed' ),
+			'pre-order'       => array( 'pre-order' ),
+			'pre-ordered'     => array( 'pre-ordered' ),
+			'partial-payment' => array( 'partial-payment' ),
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Triggers/NewReviewNotificationTriggerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Triggers/NewReviewNotificationTriggerTest.php
new file mode 100644
index 00000000000..0f28507f4f7
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Triggers/NewReviewNotificationTriggerTest.php
@@ -0,0 +1,173 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Triggers;
+
+use Automattic\WooCommerce\Internal\PushNotifications\Services\PendingNotificationStore;
+use Automattic\WooCommerce\Internal\PushNotifications\Triggers\NewReviewNotificationTrigger;
+use WC_Helper_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the NewReviewNotificationTrigger class.
+ */
+class NewReviewNotificationTriggerTest extends WC_Unit_Test_Case {
+	/**
+	 * An instance of NewReviewNotificationTrigger.
+	 *
+	 * @var NewReviewNotificationTrigger
+	 */
+	private $trigger;
+
+	/**
+	 * The notification store used by the trigger.
+	 *
+	 * @var PendingNotificationStore
+	 */
+	private $store;
+
+	/**
+	 * A test product ID.
+	 *
+	 * @var int
+	 */
+	private int $product_id;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->store = new PendingNotificationStore();
+		$this->store->register();
+
+		wc_get_container()->replace( PendingNotificationStore::class, $this->store );
+		wc_get_container()->reset_all_resolved();
+
+		$this->trigger = new NewReviewNotificationTrigger( $this->store );
+
+		$product          = WC_Helper_Product::create_simple_product();
+		$this->product_id = $product->get_id();
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		remove_action( 'comment_post', array( $this->trigger, 'on_comment_post' ) );
+		remove_action( 'shutdown', array( $this->store, 'dispatch_all' ) );
+
+		$this->reset_container_replacements();
+		wc_get_container()->reset_all_resolved();
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Should register the comment_post hook.
+	 */
+	public function test_register_adds_comment_post_hook(): void {
+		$this->trigger->register();
+
+		$this->assertNotFalse(
+			has_action(
+				'comment_post',
+				array( $this->trigger, 'on_comment_post' )
+			),
+			'comment_post hook should be registered'
+		);
+	}
+
+	/**
+	 * @testdox Should add a notification for an approved product review.
+	 */
+	public function test_adds_notification_for_approved_review(): void {
+		$commentdata = $this->build_review_data( $this->product_id );
+
+		$this->trigger->on_comment_post( 1, 1, $commentdata );
+
+		$this->assertSame( 1, $this->store->count() );
+	}
+
+	/**
+	 * @testdox Should add a notification for an unapproved product review.
+	 */
+	public function test_adds_notification_for_unapproved_review(): void {
+		$commentdata = $this->build_review_data( $this->product_id );
+
+		$this->trigger->on_comment_post( 1, 0, $commentdata );
+
+		$this->assertSame( 1, $this->store->count() );
+	}
+
+	/**
+	 * @testdox Should not add a notification for a spam review.
+	 */
+	public function test_ignores_spam_review(): void {
+		$commentdata = $this->build_review_data( $this->product_id );
+
+		$this->trigger->on_comment_post( 1, 'spam', $commentdata );
+
+		$this->assertSame( 0, $this->store->count() );
+	}
+
+	/**
+	 * @testdox Should not add a notification for a regular comment that is not a review.
+	 */
+	public function test_ignores_non_review_comment(): void {
+		$commentdata = array(
+			'comment_post_ID'  => $this->product_id,
+			'comment_author'   => 'test',
+			'comment_content'  => 'A regular comment.',
+			'comment_approved' => 1,
+			'comment_type'     => '',
+		);
+
+		$this->trigger->on_comment_post( 1, 1, $commentdata );
+
+		$this->assertSame( 0, $this->store->count() );
+	}
+
+	/**
+	 * @testdox Should not add a notification for a review on a non-product post.
+	 */
+	public function test_ignores_review_on_non_product(): void {
+		$post_id = wp_insert_post(
+			array(
+				'post_title'  => 'A blog post',
+				'post_type'   => 'post',
+				'post_status' => 'publish',
+			)
+		);
+
+		$commentdata = array(
+			'comment_post_ID'  => $post_id,
+			'comment_author'   => 'test',
+			'comment_content'  => 'A review on a blog post.',
+			'comment_approved' => 1,
+			'comment_type'     => 'review',
+		);
+
+		$this->trigger->on_comment_post( 1, 1, $commentdata );
+
+		$this->assertSame( 0, $this->store->count() );
+	}
+
+	/**
+	 * Builds the comment data array for a product review.
+	 *
+	 * @param int $product_id The product ID.
+	 * @return array The comment data.
+	 */
+	private function build_review_data( int $product_id ): array {
+		return array(
+			'comment_post_ID'  => $product_id,
+			'comment_author'   => 'Test Reviewer',
+			'comment_content'  => 'Great product!',
+			'comment_approved' => 1,
+			'comment_type'     => 'review',
+		);
+	}
+}