Commit 0a9f6280b20 for woocommerce

commit 0a9f6280b20343b1b64afe62050fe4887ebbfb71
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Wed Sep 23 21:15:33 2026 +0300

    Report stock notification save failures instead of silent success (#68812)

    * fix: propagate stock notification data store write errors from save()

    * fix: drop resolved WC_Data_Store return-type entries from PHPStan baseline

    * fix: stop treating a zero-row update as a notification write failure

    * fix: translate stock notification data store error messages

    * fix: check save() result directly when cancelling a stock notification

diff --git a/plugins/woocommerce/changelog/wooplug-7732-stock-notification-save-errors b/plugins/woocommerce/changelog/wooplug-7732-stock-notification-save-errors
new file mode 100644
index 00000000000..cd38099eeb1
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-7732-stock-notification-save-errors
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Surface failed database writes when saving a back in stock notification instead of reporting success.
diff --git a/plugins/woocommerce/includes/class-wc-data-store.php b/plugins/woocommerce/includes/class-wc-data-store.php
index 89281adc573..c6d0ad58f86 100644
--- a/plugins/woocommerce/includes/class-wc-data-store.php
+++ b/plugins/woocommerce/includes/class-wc-data-store.php
@@ -181,9 +181,10 @@ class WC_Data_Store {
 	 *
 	 * @since 3.0.0
 	 * @param WC_Data $data WooCommerce data instance.
+	 * @return mixed Whatever the underlying data store returns. Most core data stores return nothing; some return a WP_Error on failure.
 	 */
 	public function create( &$data ) {
-		$this->instance->create( $data );
+		return $this->instance->create( $data );
 	}

 	/**
@@ -191,9 +192,10 @@ class WC_Data_Store {
 	 *
 	 * @since 3.0.0
 	 * @param WC_Data $data WooCommerce data instance.
+	 * @return mixed Whatever the underlying data store returns. Most core data stores return nothing; some return a WP_Error on failure.
 	 */
 	public function update( &$data ) {
-		$this->instance->update( $data );
+		return $this->instance->update( $data );
 	}

 	/**
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index d108826542f..8d4f5a6ce09 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -10897,12 +10897,6 @@ parameters:
 			count: 1
 			path: includes/class-wc-customer.php

-		-
-			message: '#^Method WC_Data_Store\:\:create\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: includes/class-wc-data-store.php
-
 		-
 			message: '#^Method WC_Data_Store\:\:delete\(\) has no return type specified\.$#'
 			identifier: missingType.return
@@ -10927,12 +10921,6 @@ parameters:
 			count: 1
 			path: includes/class-wc-data-store.php

-		-
-			message: '#^Method WC_Data_Store\:\:update\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: includes/class-wc-data-store.php
-
 		-
 			message: '#^PHPDoc tag @param for parameter \$objects contains unresolvable type\.$#'
 			identifier: parameter.unresolvableType
diff --git a/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php b/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php
index 6cc1e782235..6d0b2afc26b 100644
--- a/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php
@@ -190,7 +190,7 @@ CREATE TABLE $meta_table_name (
 		);

 		if ( false === $insert ) {
-			return new \WP_Error( 'db_insert_error', 'Could not insert stock notification into the database.' );
+			return new \WP_Error( 'db_insert_error', __( 'Could not insert stock notification into the database.', 'woocommerce' ) );
 		}

 		$notification_id = (int) $wpdb->insert_id;
@@ -260,7 +260,7 @@ CREATE TABLE $meta_table_name (
 		global $wpdb;

 		if ( 0 === $notification->get_id() ) {
-			return new \WP_Error( 'invalid_stock_notification', 'Invalid notification ID.' );
+			return new \WP_Error( 'invalid_stock_notification', __( 'Invalid notification ID.', 'woocommerce' ) );
 		}

 		$changes = $notification->get_changes();
@@ -293,11 +293,7 @@ CREATE TABLE $meta_table_name (
 			);

 			if ( false === $result ) {
-				return new \WP_Error( 'db_update_error', 'Could not update stock notification in the database.' );
-			}
-
-			if ( 0 === $result ) {
-				return new \WP_Error( 'db_update_error', 'Invalid notification ID.' );
+				return new \WP_Error( 'db_update_error', __( 'Could not update stock notification in the database.', 'woocommerce' ) );
 			}
 		}

diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php b/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php
index ce30000bd0d..a0adbdfde78 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php
@@ -536,12 +536,7 @@ class MyAccountEndpoint {
 		$notification->set_cancellation_source( NotificationCancellationSource::USER );
 		$notification->set_date_cancelled( time() );

-		$notification->save();
-
-		// `WC_Data_Store::update()` drops the data store's return value, so a failed write
-		// reaches us as a successful save. Read the row back to confirm it really changed.
-		$saved = Factory::get_notification( $notification->get_id() );
-		if ( ! $saved instanceof Notification || NotificationStatus::CANCELLED !== $saved->get_status() ) {
+		if ( is_wp_error( $notification->save() ) ) {
 			return new \WP_Error( 'wc_bis_cancel_failed', __( 'We could not cancel that back in stock notification. Please try again.', 'woocommerce' ) );
 		}

diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Notification.php b/plugins/woocommerce/src/Internal/StockNotifications/Notification.php
index d4f41a52260..796a5e24da5 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Notification.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Notification.php
@@ -370,9 +370,13 @@ class Notification extends \WC_Data {
 		}

 		if ( $this->get_id() ) {
-			$this->data_store->update( $this );
+			$result = $this->data_store->update( $this );
 		} else {
-			$this->data_store->create( $this );
+			$result = $this->data_store->create( $this );
+		}
+
+		if ( is_wp_error( $result ) ) {
+			return $result;
 		}

 		return $this->get_id();
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php
index 3c80440b3fe..1e488ad0054 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php
@@ -582,50 +582,6 @@ class MyAccountEndpointTests extends \WC_Unit_Test_Case {
 		$this->assertCount( 1, \wc_get_notices( 'error' ) );
 	}

-	/**
-	 * A failed database write reports an error instead of telling the customer the
-	 * notification was cancelled.
-	 */
-	public function test_cancel_reports_an_error_when_the_save_fails(): void {
-		$user_id = $this->factory->user->create( array( 'role' => 'customer' ) );
-		\wp_set_current_user( $user_id );
-		$notification = $this->create_notification( $user_id, NotificationStatus::ACTIVE );
-
-		$this->simulate_action_request( MyAccountEndpoint::ACTION_CANCEL, $notification->get_id(), true );
-
-		$neutralize_update = array( $this, 'neutralize_notification_update' );
-		add_filter( 'query', $neutralize_update );
-		try {
-			$this->run_action_expecting_redirect();
-		} finally {
-			remove_filter( 'query', $neutralize_update );
-		}
-
-		$this->assertCount( 1, \wc_get_notices( 'error' ) );
-		$this->assertEmpty( \wc_get_notices( 'success' ) );
-
-		$reloaded = Factory::get_notification( $notification->get_id() );
-		$this->assertInstanceOf( Notification::class, $reloaded );
-		$this->assertSame( NotificationStatus::ACTIVE, $reloaded->get_status() );
-	}
-
-	/**
-	 * Make an UPDATE against the stock notifications table match no rows, so the data
-	 * store reports the failure the way a real database error would.
-	 *
-	 * @param string $query The query about to run.
-	 * @return string
-	 */
-	public function neutralize_notification_update( $query ) {
-		global $wpdb;
-
-		if ( is_string( $query ) && 0 === stripos( $query, 'UPDATE' ) && false !== stripos( $query, $wpdb->prefix . 'wc_stock_notifications' ) ) {
-			return $query . ' AND 1 = 0';
-		}
-
-		return $query;
-	}
-
 	/**
 	 * User A cannot cancel user B's notification even when the nonce validates against B's action name
 	 * (WordPress nonces bind to the current user, so this effectively asserts the ownership check).
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
index 202e941b437..251b2330f3f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
@@ -3,8 +3,12 @@
 declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Tests\Internal\StockNotifications;

+use Automattic\WooCommerce\Internal\DataStores\StockNotifications\StockNotificationsDataStore;
+use Automattic\WooCommerce\Internal\DataStores\StockNotifications\StockNotificationsMetaDataStore;
 use Automattic\WooCommerce\Internal\StockNotifications\Notification;
 use Automattic\WooCommerce\Internal\StockNotifications\Config;
+use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
+use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;

 /**
  * NotificationTests data tests.
@@ -296,4 +300,79 @@ class NotificationTests extends \WC_Unit_Test_Case {
 		$this->assertInstanceOf( \WP_Error::class, $result );
 		$this->assertSame( 'User Email is invalid.', $result->get_error_message() );
 	}
+
+	/**
+	 * @testdox save() should return the data store error when the insert fails.
+	 */
+	public function test_save_returns_data_store_error_when_create_fails(): void {
+		$this->use_failing_data_store();
+
+		$notification = new Notification();
+		$notification->set_product_id( 1 );
+		$notification->set_user_email( 'foo@bar.com' );
+
+		$result = $notification->save();
+
+		$this->assertInstanceOf( \WP_Error::class, $result );
+		$this->assertSame( 'db_insert_error', $result->get_error_code() );
+	}
+
+	/**
+	 * @testdox save() should return the data store error when the update fails.
+	 */
+	public function test_save_returns_data_store_error_when_update_fails(): void {
+		$notification = new Notification();
+		$notification->set_product_id( 1 );
+		$notification->set_user_email( 'foo@bar.com' );
+		$notification->save();
+
+		$this->use_failing_data_store();
+
+		$stored = new Notification( $notification->get_id() );
+		$stored->set_status( NotificationStatus::CANCELLED );
+
+		$result = $stored->save();
+
+		$this->assertInstanceOf( \WP_Error::class, $result );
+		$this->assertSame( 'db_update_error', $result->get_error_code() );
+	}
+
+	/**
+	 * Swap the stock notification data store for one whose writes always fail.
+	 */
+	private function use_failing_data_store(): void {
+		$store = new class() extends StockNotificationsDataStore {
+			/**
+			 * Fail every insert.
+			 *
+			 * @param Notification $notification The data object to create.
+			 * @return \WP_Error
+			 */
+			public function create( &$notification ) {
+				return new \WP_Error( 'db_insert_error', 'Could not insert stock notification into the database.' );
+			}
+
+			/**
+			 * Fail every update.
+			 *
+			 * @param Notification $notification The data object to update.
+			 * @return \WP_Error
+			 */
+			public function update( &$notification ) {
+				return new \WP_Error( 'db_update_error', 'Could not update stock notification in the database.' );
+			}
+		};
+
+		$store->init(
+			wc_get_container()->get( StockNotificationsMetaDataStore::class ),
+			wc_get_container()->get( DatabaseUtil::class )
+		);
+
+		add_filter(
+			'woocommerce_stock_notification_data_store',
+			function () use ( $store ) {
+				return $store;
+			}
+		);
+	}
 }