Commit 45ca31b75aa for woocommerce

commit 45ca31b75aa8413a4cd428a1ef12e5ca952a343b
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Thu Oct 15 19:34:03 2026 +0300

    Record back-in-stock privacy erasures with their own cancellation source and keep sent rows sent (#69829)

    fix: record privacy erasures with their own cancellation source and keep sent rows sent

    The personal data eraser cancelled every matching back-in-stock sign-up
    with the 'user' cancellation source, making GDPR erasures look like
    voluntary opt-outs, and it also flipped already Sent rows to Cancelled.
    Add a 'privacy' cancellation source, use it for Active and Pending
    rows, and leave Sent and Cancelled rows at their status while still
    anonymizing them.

diff --git a/plugins/woocommerce/changelog/fix-wooplug-7987-bis-privacy-eraser-source b/plugins/woocommerce/changelog/fix-wooplug-7987-bis-privacy-eraser-source
new file mode 100644
index 00000000000..2188bbcfdf6
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7987-bis-privacy-eraser-source
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep erased back-in-stock sign-ups distinct from customer opt-outs and leave sent ones marked as sent.
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Enums/NotificationCancellationSource.php b/plugins/woocommerce/src/Internal/StockNotifications/Enums/NotificationCancellationSource.php
index c67521dae4f..f11afa5732d 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Enums/NotificationCancellationSource.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Enums/NotificationCancellationSource.php
@@ -30,6 +30,15 @@ final class NotificationCancellationSource {
 	 */
 	public const SYSTEM = 'system';

+	/**
+	 * Privacy cancellation source, set when a personal data erasure request anonymizes the notification.
+	 *
+	 * @since 11.3.0
+	 *
+	 * @var string
+	 */
+	public const PRIVACY = 'privacy';
+
 	/**
 	 * Get valid cancellation sources.
 	 *
@@ -40,6 +49,7 @@ final class NotificationCancellationSource {
 			self::ADMIN,
 			self::USER,
 			self::SYSTEM,
+			self::PRIVACY,
 		);
 	}
 }
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Privacy/PrivacyEraser.php b/plugins/woocommerce/src/Internal/StockNotifications/Privacy/PrivacyEraser.php
index 16bb72b407d..2489ef7b61a 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Privacy/PrivacyEraser.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Privacy/PrivacyEraser.php
@@ -42,7 +42,8 @@ class PrivacyEraser extends \WC_Abstract_Privacy {
 	/**
 	 * Erase customer stock notification data for a given email address and the account it belongs to.
 	 *
-	 * This method anonymizes the user email and sets the status of the notifications to 'cancelled'.
+	 * This method anonymizes the user email and cancels Active and Pending notifications with the
+	 * 'privacy' cancellation source. Sent and already Cancelled notifications keep their status.
 	 *
 	 * @param string $email_address The email address to erase data for.
 	 *
@@ -90,9 +91,11 @@ class PrivacyEraser extends \WC_Abstract_Privacy {
 			$anonymous_email = wp_privacy_anonymize_data( 'email', $email_address );
 			$notification->set_user_email( $anonymous_email );
 			$notification->set_user_id( 0 );
-			$notification->set_status( NotificationStatus::CANCELLED );
-			$notification->set_cancellation_source( NotificationCancellationSource::USER );
-			$notification->set_date_cancelled( time() );
+			if ( in_array( $notification->get_status(), array( NotificationStatus::ACTIVE, NotificationStatus::PENDING ), true ) ) {
+				$notification->set_status( NotificationStatus::CANCELLED );
+				$notification->set_cancellation_source( NotificationCancellationSource::PRIVACY );
+				$notification->set_date_cancelled( time() );
+			}
 			$notification->update_meta_data( '_anonymized', 'yes' );
 			$notification->update_meta_data( 'verification_action_key', '' );
 			$notification->update_meta_data( 'unsubscribe_action_key', '' );
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Privacy/PrivacyEraserTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Privacy/PrivacyEraserTests.php
index 0dcb9771bd6..2664324fb70 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Privacy/PrivacyEraserTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Privacy/PrivacyEraserTests.php
@@ -3,6 +3,7 @@
 declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Tests\Internal\StockNotifications\Privacy;

+use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationCancellationSource;
 use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
 use Automattic\WooCommerce\Internal\StockNotifications\Privacy\PrivacyEraser;
 use Automattic\WooCommerce\Internal\StockNotifications\Notification;
@@ -48,6 +49,53 @@ class PrivacyEraserTests extends \WC_Unit_Test_Case {

 		$this->assertEquals( $anonymous_notification->get_user_email(), wp_privacy_anonymize_data( 'email', '' ) );
 		$this->assertEquals( NotificationStatus::CANCELLED, $anonymous_notification->get_status() );
+		$this->assertEquals( NotificationCancellationSource::PRIVACY, $anonymous_notification->get_cancellation_source() );
+		$this->assertNotNull( $anonymous_notification->get_date_cancelled() );
+	}
+
+	/**
+	 * @testdox Should keep a sent notification sent, with its sent date, while still anonymizing it.
+	 */
+	public function test_privacy_eraser_keeps_sent_notification_sent(): void {
+		$notification = new Notification();
+		$notification->set_user_email( 'jon@doe.com' );
+		$notification->set_product_id( 1 );
+		$notification->set_status( NotificationStatus::SENT );
+		$notification->set_date_notified( time() - DAY_IN_SECONDS );
+		$notification_id = $notification->save();
+		$date_notified   = ( new Notification( $notification_id ) )->get_date_notified();
+
+		$response = PrivacyEraser::erase_notification_data( 'jon@doe.com' );
+		$this->assertTrue( $response['items_removed'] );
+
+		$updated = new Notification( $notification_id );
+		$this->assertEquals( NotificationStatus::SENT, $updated->get_status() );
+		$this->assertNotNull( $date_notified );
+		$this->assertEquals( $date_notified->getTimestamp(), $updated->get_date_notified()->getTimestamp() );
+		$this->assertNull( $updated->get_cancellation_source() );
+		$this->assertNull( $updated->get_date_cancelled() );
+		$this->assertEquals( wp_privacy_anonymize_data( 'email', '' ), $updated->get_user_email() );
+		$this->assertSame( 0, $updated->get_user_id() );
+		$this->assertEquals( 'yes', $updated->get_meta( '_anonymized' ) );
+	}
+
+	/**
+	 * @testdox Should keep the cancellation source of a notification the user already cancelled.
+	 */
+	public function test_privacy_eraser_keeps_existing_cancellation_source(): void {
+		$notification = new Notification();
+		$notification->set_user_email( 'jon@doe.com' );
+		$notification->set_product_id( 1 );
+		$notification->set_status( NotificationStatus::CANCELLED );
+		$notification->set_cancellation_source( NotificationCancellationSource::USER );
+		$notification_id = $notification->save();
+
+		PrivacyEraser::erase_notification_data( 'jon@doe.com' );
+
+		$updated = new Notification( $notification_id );
+		$this->assertEquals( NotificationStatus::CANCELLED, $updated->get_status() );
+		$this->assertEquals( NotificationCancellationSource::USER, $updated->get_cancellation_source() );
+		$this->assertEquals( wp_privacy_anonymize_data( 'email', '' ), $updated->get_user_email() );
 	}

 	/**