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',
+ );
+ }
+}