Commit 454ae05345 for woocommerce

commit 454ae05345017e7523ed671d6110932e6580b591
Author: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
Date:   Wed Feb 11 09:36:26 2026 +0100

    Move email context filter to allow overwrite values from core (#63231)

diff --git a/plugins/woocommerce/changelog/wooprd-2246-preserve-wp_user b/plugins/woocommerce/changelog/wooprd-2246-preserve-wp_user
new file mode 100644
index 0000000000..21e1f3dc23
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooprd-2246-preserve-wp_user
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Update woocommerce_email_editor_integration_personalizer_context_data filter to allow overwrite values from core
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/TransactionalEmailPersonalizer.php b/plugins/woocommerce/src/Internal/EmailEditor/TransactionalEmailPersonalizer.php
index a042a569bf..b27e9f9057 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/TransactionalEmailPersonalizer.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/TransactionalEmailPersonalizer.php
@@ -67,20 +67,6 @@ class TransactionalEmailPersonalizer {
 	public function prepare_context_data( array $previous_context, \WC_Email $email ): array {
 		$context = $previous_context;

-		/**
-		 * Filters the context data for email personalization.
-		 *
-		 * @since 10.5.0
-		 * @param array     $context Previous version of context data.
-		 * @param \WC_Email $email The WooCommerce email object.
-		 * @return array Context data for personalization
-		 */
-		$context = apply_filters( 'woocommerce_email_editor_integration_personalizer_context_data', $context, $email );
-
-		if ( ! is_array( $context ) ) {
-			$context = $previous_context;
-		}
-
 		$context['recipient_email'] = $email->get_recipient();
 		$context['order']           = $email->object instanceof \WC_Order ? $email->object : null;
 		// For emails of type new_user or reset_password we want to set user directly from the object.
@@ -93,6 +79,25 @@ class TransactionalEmailPersonalizer {
 		}
 		$context['wc_email'] = $email;

+		$core_context = $context;
+
+		/**
+		 * Filters the context data for email personalization.
+		 *
+		 * This filter fires after core defaults are set, allowing extensions
+		 * to override values like wp_user for custom email types (e.g., WooCommerce Bookings).
+		 *
+		 * @since 10.5.0
+		 * @param array     $context Context data including core defaults.
+		 * @param \WC_Email $email The WooCommerce email object.
+		 * @return array Context data for personalization
+		 */
+		$context = apply_filters( 'woocommerce_email_editor_integration_personalizer_context_data', $context, $email );
+
+		if ( ! is_array( $context ) ) {
+			$context = $core_context;
+		}
+
 		return $context;
 	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/TransactionalEmailPersonalizerTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/TransactionalEmailPersonalizerTest.php
new file mode 100644
index 0000000000..aeecb2af52
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/TransactionalEmailPersonalizerTest.php
@@ -0,0 +1,135 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\EmailEditor;
+
+use Automattic\WooCommerce\Internal\EmailEditor\TransactionalEmailPersonalizer;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the TransactionalEmailPersonalizer class.
+ */
+class TransactionalEmailPersonalizerTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var TransactionalEmailPersonalizer
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->sut = new TransactionalEmailPersonalizer();
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		remove_all_filters( 'woocommerce_email_editor_integration_personalizer_context_data' );
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Should preserve wp_user set by the filter when core cannot derive it from the email object.
+	 */
+	public function test_wp_user_set_by_filter_is_preserved(): void {
+		$mock_user  = $this->createMock( \WP_User::class );
+		$mock_email = $this->createMock( \WC_Email::class );
+		$mock_email->method( 'get_recipient' )->willReturn( 'test@example.com' );
+		$mock_email->object = new \stdClass();
+
+		add_filter(
+			'woocommerce_email_editor_integration_personalizer_context_data',
+			function ( $context ) use ( $mock_user ) {
+				$context['wp_user'] = $mock_user;
+				return $context;
+			}
+		);
+
+		$result = $this->sut->prepare_context_data( array(), $mock_email );
+
+		$this->assertSame( $mock_user, $result['wp_user'], 'wp_user set by the filter should be preserved' );
+	}
+
+	/**
+	 * @testdox Should set wp_user from WP_User email object when no filter overrides it.
+	 */
+	public function test_wp_user_set_from_wp_user_object(): void {
+		$mock_user  = $this->createMock( \WP_User::class );
+		$mock_email = $this->createMock( \WC_Email::class );
+		$mock_email->method( 'get_recipient' )->willReturn( 'test@example.com' );
+		$mock_email->object = $mock_user;
+
+		$result = $this->sut->prepare_context_data( array(), $mock_email );
+
+		$this->assertSame( $mock_user, $result['wp_user'], 'wp_user should be set from WP_User email object' );
+	}
+
+	/**
+	 * @testdox Should set wp_user to null when email object is not WP_User or WC_Order and no filter overrides.
+	 */
+	public function test_wp_user_null_for_unknown_object_type(): void {
+		$mock_email = $this->createMock( \WC_Email::class );
+		$mock_email->method( 'get_recipient' )->willReturn( 'test@example.com' );
+		$mock_email->object = new \stdClass();
+
+		$result = $this->sut->prepare_context_data( array(), $mock_email );
+
+		$this->assertNull( $result['wp_user'], 'wp_user should be null when object type is unknown and no filter overrides' );
+	}
+
+	/**
+	 * @testdox Should provide core defaults to the filter callback.
+	 */
+	public function test_filter_receives_core_defaults(): void {
+		$received_context = null;
+		$mock_email       = $this->createMock( \WC_Email::class );
+		$mock_email->method( 'get_recipient' )->willReturn( 'test@example.com' );
+		$mock_email->object = new \stdClass();
+
+		add_filter(
+			'woocommerce_email_editor_integration_personalizer_context_data',
+			function ( $context ) use ( &$received_context ) {
+				$received_context = $context;
+				return $context;
+			}
+		);
+
+		$this->sut->prepare_context_data( array(), $mock_email );
+
+		$this->assertArrayHasKey( 'recipient_email', $received_context, 'Filter should receive recipient_email' );
+		$this->assertSame( 'test@example.com', $received_context['recipient_email'] );
+		$this->assertArrayHasKey( 'wp_user', $received_context, 'Filter should receive wp_user' );
+		$this->assertArrayHasKey( 'wc_email', $received_context, 'Filter should receive wc_email' );
+		$this->assertArrayHasKey( 'order', $received_context, 'Filter should receive order' );
+	}
+
+	/**
+	 * @testdox Should fall back to core context when filter returns non-array.
+	 */
+	public function test_fallback_to_core_context_when_filter_returns_non_array(): void {
+		$mock_email = $this->createMock( \WC_Email::class );
+		$mock_email->method( 'get_recipient' )->willReturn( 'test@example.com' );
+		$mock_email->object = new \stdClass();
+
+		add_filter(
+			'woocommerce_email_editor_integration_personalizer_context_data',
+			function () {
+				return 'not_an_array';
+			}
+		);
+
+		$result = $this->sut->prepare_context_data( array(), $mock_email );
+
+		$this->assertArrayHasKey( 'recipient_email', $result, 'Fallback should contain core defaults' );
+		$this->assertSame( 'test@example.com', $result['recipient_email'] );
+		$this->assertArrayHasKey( 'wp_user', $result, 'Fallback should contain wp_user from core' );
+		$this->assertArrayHasKey( 'wc_email', $result, 'Fallback should contain wc_email from core' );
+		$this->assertArrayHasKey( 'order', $result, 'Fallback should contain order from core' );
+	}
+}