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.
 	 *