Commit 357bb51d025 for woocommerce
commit 357bb51d0250dcce2d5e4ac2bdb4851d95d923ae
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date: Mon Nov 2 17:54:53 2026 +0200
Add a pre-send filter and a sent action to the back-in-stock notification send path (#69879)
* fix: mark back-in-stock sign-ups as sent only when the email is sent
CustomerStockNotificationEmail::trigger() ignored the result of send(),
so the notifications processor marked every sign-up Sent and logged
"Failed: 0" even when the email was disabled or wp_mail() failed.
trigger() and EmailManager::send_stock_notification_email() now return
whether the email went out, and the processor marks the sign-up Sent
only on success, counting the rest as failed.
* chore: drop stale PHPStan baseline entry for the stock notification email trigger return type
* test: report emails as sent in the e2e environment
wp-env has no mail transport, so wp_mail() returns false after WP Mail Logging
has captured the message. Now that a failed send cancels a back-in-stock
sign-up, the e2e specs saw every notification as cancelled. Short-circuit
wp_mail() with pre_wp_mail so the specs observe the production outcome.
* test: scope the e2e wp_mail short-circuit to the back-in-stock batch
The global pre_wp_mail override in the helper plugin broke the email settings
and email editor specs, which assert the no-mailer failure path. Set the filter
through the e2e-filters cookie only when a spec drains the notifications queue.
* style: fix Prettier indentation in back-in-stock e2e utils
* fix: only mark a sign-up sent from the admin edit page when the email goes out
The "Send notification" action marked the sign-up as sent regardless of the
wp_mail() result. Check the return value, keep the status untouched on failure
so the admin can retry, and show an error notice instead.
* feat: add pre-send filter and sent action to stock notification send path
Both the batch processor and the admin "Send now" action now go through
EmailManager::deliver_stock_notification(), which runs the
woocommerce_customer_stock_notification_pre_send filter: null sends the
Core email, true marks the notification Sent without one, and false or a
WP_Error takes the failure branch. Once the row is saved as Sent, both
paths fire woocommerce_customer_stock_notification_sent.
* fix: narrow the product passed to the stock notification sent action
Notification::get_product() returns WC_Product|false, so PHPStan rejected
passing it straight to an action documented as taking WC_Product. Resolve
it once and only fire woocommerce_customer_stock_notification_sent when a
product exists, matching the guard in deliver_stock_notification().
* fix: keep a throwing sent-action callback from breaking the send path
Wrap the woocommerce_customer_stock_notification_sent action in try/catch in
both the batch processor and the admin "Send now" action, logging the failure
to wc-customer-stock-notifications. A third-party callback that throws no
longer aborts the rest of the batch or crashes the edit page.
Type the processor logger as WC_Logger_Interface so PHPStan resolves it; the
three stale baseline entries for the unknown Logger class are dropped.
* fix: contain delivery failures on Send now and fire the sent action only after a successful save
A throwing woocommerce_customer_stock_notification_pre_send callback on the
admin "Send now" path is now caught, logged and routed to the existing
failure notice, matching the batch processor. Both send paths now fire
woocommerce_customer_stock_notification_sent only when Notification::save()
did not return a WP_Error, so the action never reports a Sent state that was
not persisted.
* test: mark the unused pre-send filter argument as unused
* docs: bump stock notification pre-send hooks @since to 11.4.0
* fix: log every failed stock notification delivery, not only WP_Error ones
The pre-send filter can return false or any unexpected type and the
customer would be cancelled without a trace. Log one warning for every
non-true outcome, naming the WP_Error message or the returned type.
* fix: log delivery exceptions in the stock notification batch path
The batch catch block dropped the exception, so a throwing pre-send
callback cancelled the sign-up without a trace. Log it at error level
like the admin Send now path already does.
diff --git a/plugins/woocommerce/changelog/add-bis-pre-send-filter-sent-action b/plugins/woocommerce/changelog/add-bis-pre-send-filter-sent-action
new file mode 100644
index 00000000000..8662647ac0b
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-bis-pre-send-filter-sent-action
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a pre-send filter and a sent action to the stock notification send path so integrations can take over delivery.
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Admin/NotificationEditPage.php b/plugins/woocommerce/src/Internal/StockNotifications/Admin/NotificationEditPage.php
index 94317bc00fa..5582335c40e 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Admin/NotificationEditPage.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Admin/NotificationEditPage.php
@@ -112,10 +112,48 @@ class NotificationEditPage {
if ( ! $product || ! $product->is_in_stock() ) {
$notice_message = __( 'Failed to send notification. Please make sure that the listed product is available.', 'woocommerce' );
NotificationsPage::add_notice( $notice_message, 'error' );
- } elseif ( $this->email_manager->send_stock_notification_email( $notification ) ) {
+ break;
+ }
+
+ try {
+ $is_sent = $this->email_manager->deliver_stock_notification( $notification );
+ } catch ( \Throwable $e ) {
+ $is_sent = false;
+ wc_get_logger()->error(
+ sprintf( 'Delivery of notification %d failed: %s', $notification->get_id(), $e->getMessage() ),
+ array( 'source' => 'wc-customer-stock-notifications' )
+ );
+ }
+
+ if ( $is_sent ) {
$notification->set_status( NotificationStatus::SENT );
$notification->set_date_notified( time() );
- $notification->save();
+ $saved = $notification->save();
+
+ if ( is_wp_error( $saved ) ) {
+ NotificationsPage::add_notice( $saved->get_error_message(), 'error' );
+ break;
+ }
+
+ try {
+ /**
+ * Action: woocommerce_customer_stock_notification_sent
+ *
+ * @since 11.4.0
+ *
+ * Fires once a stock notification has been delivered and saved as sent.
+ *
+ * @param Notification $notification The notification that was sent.
+ * @param \WC_Product $product The product the notification is about.
+ */
+ do_action( 'woocommerce_customer_stock_notification_sent', $notification, $product );
+ } catch ( \Throwable $e ) {
+ wc_get_logger()->error(
+ sprintf( 'A woocommerce_customer_stock_notification_sent callback failed for notification %d: %s', $notification->get_id(), $e->getMessage() ),
+ array( 'source' => 'wc-customer-stock-notifications' )
+ );
+ }
+
// translators: %s user email.
$notice_message = sprintf( __( 'Notification sent to "%s".', 'woocommerce' ), $notification->get_user_email() );
NotificationsPage::add_notice( $notice_message, 'success' );
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php b/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
index 6f93d7ec2a4..a088514b552 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
@@ -230,9 +230,13 @@ class NotificationsProcessor {
}
try {
- $is_sent = $this->email_manager->send_stock_notification_email( $notification );
+ $is_sent = $this->email_manager->deliver_stock_notification( $notification );
} catch ( \Throwable $e ) {
$is_sent = false;
+ $this->logger->error(
+ sprintf( 'Delivery of notification %d failed: %s', $notification->get_id(), $e->getMessage() ),
+ array( 'source' => 'wc-customer-stock-notifications' )
+ );
}
if ( $is_sent ) {
@@ -246,7 +250,36 @@ class NotificationsProcessor {
}
// Always save the notification to reflect last attempt time.
- $notification->save();
+ $saved = $notification->save();
+ if ( is_wp_error( $saved ) ) {
+ $this->logger->error(
+ sprintf( 'Failed to save notification %d: %s', $notification->get_id(), $saved->get_error_message() ),
+ array( 'source' => 'wc-customer-stock-notifications' )
+ );
+ continue;
+ }
+
+ $notification_product = $notification->get_product();
+ if ( $is_sent && $notification_product instanceof \WC_Product ) {
+ try {
+ /**
+ * Action: woocommerce_customer_stock_notification_sent
+ *
+ * @since 11.4.0
+ *
+ * Fires once a stock notification has been delivered and saved as sent.
+ *
+ * @param Notification $notification The notification that was sent.
+ * @param WC_Product $product The product the notification is about.
+ */
+ do_action( 'woocommerce_customer_stock_notification_sent', $notification, $notification_product );
+ } catch ( \Throwable $e ) {
+ $this->logger->error(
+ sprintf( 'A woocommerce_customer_stock_notification_sent callback failed for notification %d: %s', $notification->get_id(), $e->getMessage() ),
+ array( 'source' => 'wc-customer-stock-notifications' )
+ );
+ }
+ }
}
if ( count( $notifications ) === $this->get_batch_size() ) {
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailManager.php b/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailManager.php
index 68c6dd7b606..2dde1a9840a 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailManager.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailManager.php
@@ -291,6 +291,58 @@ class EmailManager {
return $email->trigger( $notification );
}
+ /**
+ * Deliver a stock notification, letting an integration take over delivery.
+ *
+ * @since 11.4.0
+ *
+ * @param Notification $notification The notification object.
+ * @return bool Whether the notification was delivered.
+ */
+ public function deliver_stock_notification( Notification $notification ): bool {
+ $product = $notification->get_product();
+ if ( ! $product instanceof \WC_Product ) {
+ return false;
+ }
+
+ /**
+ * Filter: woocommerce_customer_stock_notification_pre_send
+ *
+ * @since 11.4.0
+ *
+ * Take over delivery of a stock notification instead of sending the Core email.
+ *
+ * @param null|bool|\WP_Error $handled Null to let Core send its own email. True when the notification was delivered
+ * externally and should be marked sent. False or a WP_Error when delivery failed.
+ * @param Notification $notification The notification being delivered.
+ * @param \WC_Product $product The product the notification is about.
+ * @return null|bool|\WP_Error
+ */
+ $handled = apply_filters( 'woocommerce_customer_stock_notification_pre_send', null, $notification, $product );
+
+ if ( null === $handled ) {
+ return $this->send_stock_notification_email( $notification );
+ }
+
+ if ( true === $handled ) {
+ return true;
+ }
+
+ $reason = is_wp_error( $handled )
+ ? $handled->get_error_message()
+ : sprintf( 'the pre-send filter returned %s instead of true, false, null or WP_Error', false === $handled ? 'false' : gettype( $handled ) );
+
+ wc_get_logger()->warning(
+ sprintf( 'Delivery of notification %d failed: %s', $notification->get_id(), $reason ),
+ array(
+ 'source' => 'wc-customer-stock-notifications',
+ 'notification_id' => $notification->get_id(),
+ )
+ );
+
+ return false;
+ }
+
/**
* Send the double opt-in verification email for a stock notification.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/NotificationEditPageTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/NotificationEditPageTests.php
index a8026e9d180..62c9f4a7847 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/NotificationEditPageTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/NotificationEditPageTests.php
@@ -10,6 +10,7 @@ use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
use Automattic\WooCommerce\Internal\StockNotifications\Frontend\NotificationManagementService;
use Automattic\WooCommerce\Internal\StockNotifications\Notification;
use Automattic\WooCommerce\Tests\Internal\StockNotifications\StockNotificationsFeatureTrait;
+use WC_Helper_Product;
/**
* Tests for the admin notification edit form handler.
@@ -99,7 +100,7 @@ class NotificationEditPageTests extends \WC_Unit_Test_Case {
* @testdox Sending a notification from the edit page marks it sent when the email goes out.
*/
public function test_send_notification_marks_sent_when_email_is_sent(): void {
- $this->email_manager->method( 'send_stock_notification_email' )->willReturn( true );
+ $this->email_manager->method( 'deliver_stock_notification' )->willReturn( true );
$notification_id = $this->submit_send_notification();
$notification = new Notification( $notification_id );
@@ -112,7 +113,7 @@ class NotificationEditPageTests extends \WC_Unit_Test_Case {
* @testdox Sending a notification from the edit page leaves it active when the email fails.
*/
public function test_send_notification_keeps_status_when_email_fails(): void {
- $this->email_manager->method( 'send_stock_notification_email' )->willReturn( false );
+ $this->email_manager->method( 'deliver_stock_notification' )->willReturn( false );
$notification_id = $this->submit_send_notification();
$notification = new Notification( $notification_id );
@@ -121,13 +122,106 @@ class NotificationEditPageTests extends \WC_Unit_Test_Case {
$this->assertSame( 'error', get_option( NotificationsPage::ADMIN_NOTICE_OPTION_NAME )['type'] );
}
+ /**
+ * @testdox "Send now" fires the sent action only when delivery succeeds.
+ *
+ * @dataProvider send_now_delivery_provider
+ *
+ * @param bool $delivered What the email manager reports back for the delivery.
+ */
+ public function test_send_notification_fires_sent_action_only_when_delivered( bool $delivered ): void {
+ $this->email_manager->method( 'deliver_stock_notification' )->willReturn( $delivered );
+
+ $sent_action_args = array();
+ add_action(
+ 'woocommerce_customer_stock_notification_sent',
+ function ( $notification, $product ) use ( &$sent_action_args ) {
+ $sent_action_args[] = array( $notification->get_id(), $product->get_id() );
+ },
+ 10,
+ 2
+ );
+
+ $notification_id = $this->submit_send_notification();
+ $product_id = ( new Notification( $notification_id ) )->get_product_id();
+
+ $this->assertSame( $delivered ? array( array( $notification_id, $product_id ) ) : array(), $sent_action_args );
+ }
+
+ /**
+ * @testdox A throwing `woocommerce_customer_stock_notification_sent` callback does not break the "Send now" action.
+ */
+ public function test_send_notification_survives_throwing_sent_action_callback(): void {
+ $this->email_manager->method( 'deliver_stock_notification' )->willReturn( true );
+ add_action(
+ 'woocommerce_customer_stock_notification_sent',
+ function () {
+ throw new \RuntimeException( 'Integration exploded.' );
+ }
+ );
+
+ $notification_id = $this->submit_send_notification();
+
+ $this->assertSame( NotificationStatus::SENT, ( new Notification( $notification_id ) )->get_status() );
+ $this->assertSame( 'success', get_option( NotificationsPage::ADMIN_NOTICE_OPTION_NAME )['type'] );
+ }
+
+ /**
+ * @testdox A throwing `woocommerce_customer_stock_notification_pre_send` callback leaves the sign-up active and shows the failure notice.
+ */
+ public function test_send_notification_survives_throwing_delivery(): void {
+ $this->email_manager->method( 'deliver_stock_notification' )->willThrowException( new \RuntimeException( 'Provider exploded.' ) );
+
+ $notification_id = $this->submit_send_notification();
+
+ $this->assertSame( NotificationStatus::ACTIVE, ( new Notification( $notification_id ) )->get_status() );
+ $this->assertSame( 'error', get_option( NotificationsPage::ADMIN_NOTICE_OPTION_NAME )['type'] );
+ }
+
+ /**
+ * @testdox "Send now" does not fire the sent action when the sign-up cannot be saved as sent.
+ */
+ public function test_send_notification_skips_sent_action_when_save_fails(): void {
+ $this->email_manager->method( 'deliver_stock_notification' )->willReturnCallback(
+ function ( Notification $notification ) {
+ $notification->set_user_email( 'not-an-email' );
+ return true;
+ }
+ );
+ $sent_action_fired = false;
+ add_action(
+ 'woocommerce_customer_stock_notification_sent',
+ function () use ( &$sent_action_fired ) {
+ $sent_action_fired = true;
+ }
+ );
+
+ $notification_id = $this->submit_send_notification();
+
+ $this->assertFalse( $sent_action_fired );
+ $this->assertSame( NotificationStatus::ACTIVE, ( new Notification( $notification_id ) )->get_status() );
+ $this->assertSame( 'error', get_option( NotificationsPage::ADMIN_NOTICE_OPTION_NAME )['type'] );
+ }
+
+ /**
+ * Data provider for test_send_notification_fires_sent_action_only_when_delivered.
+ *
+ * @return array[]
+ */
+ public function send_now_delivery_provider(): array {
+ return array(
+ 'delivered' => array( true ),
+ 'failed' => array( false ),
+ );
+ }
+
/**
* Submit the "send_notification" action for a new active sign-up on an in-stock product.
*
* @return int The notification ID.
*/
private function submit_send_notification(): int {
- $product = \WC_Helper_Product::create_simple_product();
+ $product = WC_Helper_Product::create_simple_product();
$notification = new Notification();
$notification->set_user_email( 'active@test.com' );
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
index d655af10cff..d1001c78e28 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Internal\StockNotifications\Utilities\StockManagement
use Automattic\WooCommerce\Internal\StockNotifications\Notification;
use Automattic\WooCommerce\Internal\StockNotifications\Emails\EmailManager;
use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
+use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationCancellationSource;
use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
@@ -466,6 +467,163 @@ class NotificationsProcessorTests extends \WC_Unit_Test_Case {
);
}
+ /**
+ * @testdox Should mark a sign-up Sent and fire the sent action when the pre-send filter takes over delivery, and take the failure branch when it reports a failure.
+ *
+ * @dataProvider pre_send_filter_provider
+ *
+ * @param mixed $handled What the `woocommerce_customer_stock_notification_pre_send` filter returns.
+ * @param string $expected_status The status the sign-up should end up in.
+ */
+ public function test_process_batch_honours_pre_send_filter( $handled, string $expected_status ): void {
+ // Batch size 1 keeps the cycle state around after the first run, so the counters can be inspected.
+ add_filter( 'woocommerce_customer_stock_notifications_batch_size', fn() => 1 );
+ add_filter( 'woocommerce_customer_stock_notification_pre_send', fn() => $handled );
+ $sent_action_args = array();
+ add_action(
+ 'woocommerce_customer_stock_notification_sent',
+ function ( $notification, $product ) use ( &$sent_action_args ) {
+ $sent_action_args[] = array( $notification->get_id(), $product->get_id() );
+ },
+ 10,
+ 2
+ );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $notification = new Notification();
+ $notification->set_product_id( $product->get_id() );
+ $notification->set_user_email( 'test@test.com' );
+ $notification->set_status( NotificationStatus::ACTIVE );
+ $notification->save();
+
+ $this->sut->process_batch( $product->get_id() );
+
+ $notification = new Notification( $notification->get_id() );
+ $cycle_state = get_option( CycleStateService::STATE_OPTION_PREFIX . $product->get_id() );
+ $this->assertEquals( $expected_status, $notification->get_status() );
+ $this->assertEqualsWithDelta( time(), $notification->get_date_last_attempt()->getTimestamp(), 5 );
+ if ( NotificationStatus::SENT === $expected_status ) {
+ $this->assertEqualsWithDelta( time(), $notification->get_date_notified()->getTimestamp(), 5 );
+ $this->assertSame( array( array( $notification->get_id(), $product->get_id() ) ), $sent_action_args );
+ $this->assertSame( 1, $cycle_state['sent_count'] );
+ $this->assertSame( 0, $cycle_state['failed_count'] );
+ } else {
+ $this->assertEmpty( $notification->get_date_notified() );
+ $this->assertSame( NotificationCancellationSource::SYSTEM, $notification->get_cancellation_source() );
+ $this->assertSame( array(), $sent_action_args, 'The sent action must not fire for a failed delivery.' );
+ $this->assertSame( 0, $cycle_state['sent_count'] );
+ $this->assertSame( 1, $cycle_state['failed_count'] );
+ }
+
+ WC()->queue()->cancel_all( JobManager::AS_JOB_SEND_STOCK_NOTIFICATIONS );
+ }
+
+ /**
+ * Data provider for test_process_batch_honours_pre_send_filter.
+ *
+ * @return array[]
+ */
+ public function pre_send_filter_provider(): array {
+ return array(
+ 'true: handled externally' => array( true, NotificationStatus::SENT ),
+ 'false: failed' => array( false, NotificationStatus::CANCELLED ),
+ 'WP_Error: failed' => array( new \WP_Error( 'delivery_failed', 'Provider unreachable.' ), NotificationStatus::CANCELLED ),
+ );
+ }
+
+ /**
+ * @testdox A throwing `woocommerce_customer_stock_notification_sent` callback is logged and does not stop the batch.
+ */
+ public function test_process_batch_survives_throwing_sent_action_callback(): void {
+ add_filter( 'woocommerce_customer_stock_notification_pre_send', '__return_true' );
+ add_action(
+ 'woocommerce_customer_stock_notification_sent',
+ function () {
+ throw new \RuntimeException( 'Integration exploded.' );
+ }
+ );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $ids = array();
+ foreach ( array( 'first@test.com', 'second@test.com' ) as $email ) {
+ $notification = new Notification();
+ $notification->set_product_id( $product->get_id() );
+ $notification->set_user_email( $email );
+ $notification->set_status( NotificationStatus::ACTIVE );
+ $ids[] = $notification->save();
+ }
+
+ $this->sut->process_batch( $product->get_id() );
+
+ foreach ( $ids as $id ) {
+ $this->assertSame( NotificationStatus::SENT, ( new Notification( $id ) )->get_status() );
+ }
+ }
+
+ /**
+ * @testdox A throwing `woocommerce_customer_stock_notification_pre_send` callback is logged, cancels the sign-up and does not stop the batch.
+ */
+ public function test_process_batch_logs_throwing_pre_send_callback(): void {
+ add_filter(
+ 'woocommerce_customer_stock_notification_pre_send',
+ function () {
+ throw new \RuntimeException( 'Provider exploded.' );
+ }
+ );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $ids = array();
+ foreach ( array( 'first@test.com', 'second@test.com' ) as $email ) {
+ $notification = new Notification();
+ $notification->set_product_id( $product->get_id() );
+ $notification->set_user_email( $email );
+ $notification->set_status( NotificationStatus::ACTIVE );
+ $ids[] = $notification->save();
+ }
+
+ $this->sut->process_batch( $product->get_id() );
+
+ foreach ( $ids as $id ) {
+ $this->assertSame( NotificationStatus::CANCELLED, ( new Notification( $id ) )->get_status() );
+ $this->assertLogged( 'error', sprintf( 'Delivery of notification %d failed: Provider exploded.', $id ) );
+ }
+ }
+
+ /**
+ * @testdox The sent action does not fire for a sign-up that could not be saved as sent.
+ */
+ public function test_process_batch_skips_sent_action_when_save_fails(): void {
+ add_filter(
+ 'woocommerce_customer_stock_notification_pre_send',
+ function ( $handled, Notification $notification ) {
+ unset( $handled );
+ $notification->set_user_email( 'not-an-email' );
+ return true;
+ },
+ 10,
+ 2
+ );
+ $sent_action_fired = false;
+ add_action(
+ 'woocommerce_customer_stock_notification_sent',
+ function () use ( &$sent_action_fired ) {
+ $sent_action_fired = true;
+ }
+ );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $notification = new Notification();
+ $notification->set_product_id( $product->get_id() );
+ $notification->set_user_email( 'test@test.com' );
+ $notification->set_status( NotificationStatus::ACTIVE );
+ $notification->save();
+
+ $this->sut->process_batch( $product->get_id() );
+
+ $this->assertFalse( $sent_action_fired );
+ $this->assertSame( NotificationStatus::ACTIVE, ( new Notification( $notification->get_id() ) )->get_status() );
+ }
+
/**
* Test process_batch method on a product with a notification that is throttled.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Emails/EmailManagerTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Emails/EmailManagerTests.php
index 8f9e0ec4c53..48519d08c84 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Emails/EmailManagerTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Emails/EmailManagerTests.php
@@ -149,6 +149,100 @@ class EmailManagerTests extends \WC_Unit_Test_Case {
$this->assertArrayHasKey( 'WC_Email_Customer_Stock_Notification_Verified', $emails );
}
+ /**
+ * @testdox Should send the Core email when the pre-send filter returns null, and otherwise report what the filter decided.
+ *
+ * @dataProvider pre_send_filter_provider
+ *
+ * @param mixed $handled What the `woocommerce_customer_stock_notification_pre_send` filter returns.
+ * @param bool $wp_mail_result What `wp_mail()` reports back when Core sends.
+ * @param bool $expected The expected return value of deliver_stock_notification.
+ */
+ public function test_deliver_stock_notification_honours_pre_send_filter( $handled, bool $wp_mail_result, bool $expected ): void {
+ $this->wp_mail_result = $wp_mail_result;
+ $notification = $this->build_notification();
+ $received = array();
+ add_filter(
+ 'woocommerce_customer_stock_notification_pre_send',
+ function ( $pre, $filtered_notification, $product ) use ( $handled, &$received ) {
+ $received = array( $pre, $filtered_notification->get_id(), $product->get_id() );
+ return $handled;
+ },
+ 10,
+ 3
+ );
+
+ $this->assertSame( $expected, $this->sut->deliver_stock_notification( $notification ) );
+ $this->assertSame( array( null, $notification->get_id(), $notification->get_product_id() ), $received );
+ $this->assertSame( null === $handled, isset( $this->sent_to[ $notification->get_user_email() ] ), 'The Core email should only go out when the filter returns null.' );
+ }
+
+ /**
+ * Data provider for test_deliver_stock_notification_honours_pre_send_filter.
+ *
+ * @return array[]
+ */
+ public function pre_send_filter_provider(): array {
+ return array(
+ 'null: Core sends' => array( null, true, true ),
+ 'null: Core email not sent' => array( null, false, false ),
+ 'true: handled' => array( true, true, true ),
+ 'false: failed' => array( false, true, false ),
+ 'WP_Error: failed' => array( new \WP_Error( 'delivery_failed', 'Provider unreachable.' ), true, false ),
+ 'string: failed' => array( 'sent', true, false ),
+ );
+ }
+
+ /**
+ * @testdox Should log a warning naming the notification whenever the pre-send filter does not report success.
+ *
+ * @dataProvider pre_send_filter_failure_provider
+ *
+ * @param mixed $handled What the `woocommerce_customer_stock_notification_pre_send` filter returns.
+ * @param string $expected Text the logged warning must contain.
+ */
+ public function test_deliver_stock_notification_logs_a_warning_when_the_pre_send_filter_fails( $handled, string $expected ): void {
+ $notification = $this->build_notification();
+
+ $logger = $this->getMockBuilder( \WC_Logger_Interface::class )->getMock();
+ $logger->expects( $this->once() )
+ ->method( 'warning' )
+ ->with(
+ $this->logicalAnd( $this->stringContains( (string) $notification->get_id() ), $this->stringContains( $expected ) ),
+ $this->callback( fn( $context ) => 'wc-customer-stock-notifications' === $context['source'] && $notification->get_id() === $context['notification_id'] )
+ );
+ $use_logger = function () use ( $logger ) {
+ return $logger;
+ };
+ add_filter( 'woocommerce_logging_class', $use_logger );
+ $take_over = function () use ( $handled ) {
+ return $handled;
+ };
+ add_filter( 'woocommerce_customer_stock_notification_pre_send', $take_over );
+
+ $result = $this->sut->deliver_stock_notification( $notification );
+
+ remove_filter( 'woocommerce_customer_stock_notification_pre_send', $take_over );
+ remove_filter( 'woocommerce_logging_class', $use_logger );
+
+ $this->assertFalse( $result );
+ $this->assertEmpty( $this->sent_to );
+ }
+
+ /**
+ * Data provider for test_deliver_stock_notification_logs_a_warning_when_the_pre_send_filter_fails.
+ *
+ * @return array[]
+ */
+ public function pre_send_filter_failure_provider(): array {
+ return array(
+ 'false' => array( false, 'returned false' ),
+ 'WP_Error' => array( new \WP_Error( 'delivery_failed', 'Provider unreachable.' ), 'Provider unreachable.' ),
+ 'string' => array( 'sent', 'returned string' ),
+ 'integer' => array( 1, 'returned integer' ),
+ );
+ }
+
/**
* @testdox Should log a warning instead of failing silently when the email class is missing from the mailer.
*/