Commit c0875f9936f for woocommerce
commit c0875f9936f18de1c59e0098f63cd08878c0c925
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date: Thu Oct 15 18:58:46 2026 +0300
Log expected back-in-stock processing skips at info level instead of error (#69828)
* fix: log expected back-in-stock processing skips at info level
The background notifications processor logged every early termination
at error level, including normal outcomes such as a product whose stock
status went back to out of stock before the job ran. Those cases now
throw a RuntimeException from parse_product() and are logged at info,
while invalid arguments, a missing product or an invalid cycle state
still log at error. The logger property type is corrected to
WC_Logger_Interface and its stale PHPStan baseline entries are removed.
* fix: use a dedicated exception for expected back-in-stock skips
Classifying every RuntimeException as an expected skip would also downgrade
exceptions thrown by woocommerce_customer_stock_notifications_product_is_valid
callbacks to info level. NotificationsProcessor::parse_product() now throws
ExpectedSkipException for the two expected outcomes and process_batch() only
logs that type at info. Adds a test covering a throwing filter callback.
* test: use LoggerSpyTrait for back-in-stock log level assertions
diff --git a/plugins/woocommerce/changelog/fix-wooplug-7924-bis-log-level-expected-skips b/plugins/woocommerce/changelog/fix-wooplug-7924-bis-log-level-expected-skips
new file mode 100644
index 00000000000..f0b19a947d6
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7924-bis-log-level-expected-skips
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Log expected back-in-stock processing skips, such as a product that is no longer in stock, at info level instead of error.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 51e7dbd2a93..d6d46e3861f 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -65610,24 +65610,6 @@ parameters:
count: 1
path: src/Internal/StockNotifications/AsyncTasks/CycleStateService.php
- -
- message: '#^Call to method error\(\) on an unknown class Automattic\\WooCommerce\\Internal\\StockNotifications\\AsyncTasks\\Logger\.$#'
- identifier: class.notFound
- count: 2
- path: src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
-
- -
- message: '#^Property Automattic\\WooCommerce\\Internal\\StockNotifications\\AsyncTasks\\NotificationsProcessor\:\:\$logger \(Automattic\\WooCommerce\\Internal\\StockNotifications\\AsyncTasks\\Logger\) does not accept WC_Logger_Interface\.$#'
- identifier: assign.propertyType
- count: 1
- path: src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
-
- -
- message: '#^Property Automattic\\WooCommerce\\Internal\\StockNotifications\\AsyncTasks\\NotificationsProcessor\:\:\$logger has unknown class Automattic\\WooCommerce\\Internal\\StockNotifications\\AsyncTasks\\Logger as its type\.$#'
- identifier: class.notFound
- count: 1
- path: src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
-
-
message: '#^Method Automattic\\WooCommerce\\Internal\\StockNotifications\\DataRetentionController\:\:clear_daily_task\(\) has no return type specified\.$#'
identifier: missingType.return
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/ExpectedSkipException.php b/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/ExpectedSkipException.php
new file mode 100644
index 00000000000..a311ef08fdc
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/ExpectedSkipException.php
@@ -0,0 +1,15 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\StockNotifications\AsyncTasks;
+
+use RuntimeException;
+
+/**
+ * Thrown when a notifications batch stops for an expected reason, such as a product
+ * that is no longer eligible or has gone back out of stock before the job ran.
+ *
+ * @since 11.3.0
+ */
+class ExpectedSkipException extends RuntimeException {}
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php b/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
index 4ea1b09f77b..67f086029d5 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessor.php
@@ -30,7 +30,7 @@ class NotificationsProcessor {
/**
* The logger.
*
- * @var Logger
+ * @var \WC_Logger_Interface
*/
private $logger;
@@ -135,7 +135,8 @@ class NotificationsProcessor {
*
* @param int $product_id The product ID.
* @return \WC_Product
- * @throws \Exception If the product is not valid for notifications.
+ * @throws \Exception If the product is not found.
+ * @throws ExpectedSkipException If the product or its stock status is no longer eligible, which is an expected outcome.
*/
private function parse_product( int $product_id ): WC_Product {
@@ -145,11 +146,11 @@ class NotificationsProcessor {
}
if ( ! $this->eligibility_service->is_product_eligible( $product ) ) {
- throw new \Exception( sprintf( 'Product %d is not eligible for notifications.', $product->get_id() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
+ throw new ExpectedSkipException( sprintf( 'Product %d is not eligible for notifications.', $product->get_id() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
if ( ! $this->eligibility_service->is_stock_status_eligible( $product->get_stock_status() ) ) {
- throw new \Exception( sprintf( 'Product %d stock status is not eligible for notifications (i.e. not in stock).', $product->get_id() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
+ throw new ExpectedSkipException( sprintf( 'Product %d stock status is not eligible for notifications (i.e. not in stock).', $product->get_id() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
return $product;
@@ -169,7 +170,10 @@ class NotificationsProcessor {
$product = $this->parse_product( $product_id );
} catch ( \Throwable $e ) {
$product_id = (int) $product_id;
- $this->logger->error(
+ // An ineligible product or stock status is a normal outcome (e.g. stock went back out before the job ran).
+ $level = $e instanceof ExpectedSkipException ? \WC_Log_Levels::INFO : \WC_Log_Levels::ERROR;
+ $this->logger->log(
+ $level,
sprintf( 'Background process for product %s terminated. Reason: %s', $product_id, $e->getMessage() ),
array(
'source' => 'wc-customer-stock-notifications',
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 583aeb5fbe8..1fd09733126 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
@@ -13,6 +13,7 @@ use Automattic\WooCommerce\Internal\StockNotifications\Emails\EmailManager;
use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Enums\ProductStatus;
+use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
use Automattic\WooCommerce\Tests\Internal\StockNotifications\StockNotificationsFeatureTrait;
use WC_Product;
use WC_Helper_Product;
@@ -23,6 +24,7 @@ use WC_Helper_Product;
class NotificationsProcessorTests extends \WC_Unit_Test_Case {
use StockNotificationsFeatureTrait;
+ use LoggerSpyTrait;
/**
* @var NotificationsProcessor
@@ -56,6 +58,7 @@ class NotificationsProcessorTests extends \WC_Unit_Test_Case {
$wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notificationmeta" );
$wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notifications" );
$this->restore_stock_notifications_feature_option();
+ remove_all_filters( 'woocommerce_customer_stock_notifications_product_is_valid' );
parent::tearDown();
}
@@ -578,6 +581,47 @@ class NotificationsProcessorTests extends \WC_Unit_Test_Case {
);
}
+ /**
+ * Test process_batch logs an ineligible stock status at info level, as it is an expected outcome.
+ */
+ public function test_process_batch_logs_not_eligible_stock_status_at_info_level() {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+ $product->save();
+
+ $this->sut->process_batch( $product->get_id() );
+
+ $this->assertLogged( 'info', 'stock status is not eligible' );
+ $this->assertNoErrorLogged();
+ }
+
+ /**
+ * Test process_batch logs a missing product at error level.
+ */
+ public function test_process_batch_logs_missing_product_at_error_level() {
+ $this->sut->process_batch( 123 );
+
+ $this->assertLogged( 'error', 'Product 123 not found' );
+ }
+
+ /**
+ * Test process_batch logs an exception thrown by an eligibility filter callback at error level.
+ */
+ public function test_process_batch_logs_filter_exception_at_error_level() {
+ $product = WC_Helper_Product::create_simple_product();
+
+ add_filter(
+ 'woocommerce_customer_stock_notifications_product_is_valid',
+ function () {
+ throw new \RuntimeException( 'Callback failed.' );
+ }
+ );
+
+ $this->sut->process_batch( $product->get_id() );
+
+ $this->assertLogged( 'error', 'Callback failed.' );
+ }
+
/**
* Get private method.
*