Commit b0d89401ae2 for woocommerce

commit b0d89401ae22ba854690a9547fa01110fa0f4931
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Wed Oct 14 12:43:01 2026 +0300

    Fix fulfillments showing the raw shipping provider slug (#69793)

    * [WOOPLUG-7974] Show resolved shipping provider name instead of raw slug

    * Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin

    * [WOOPLUG-7974] Resolve the custom-provider sentinel name ahead of the registry

    * [WOOPLUG-7974] Guard against a non-scalar custom provider name

    * [WOOPLUG-7974] Guard against a non-scalar provider label filter result

    * [WOOPLUG-7974] Register a known provider in the display-name tests

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/69793-wooplug-7974-fulfillment-email-provider-name b/plugins/woocommerce/changelog/69793-wooplug-7974-fulfillment-email-provider-name
new file mode 100644
index 00000000000..15850860149
--- /dev/null
+++ b/plugins/woocommerce/changelog/69793-wooplug-7974-fulfillment-email-provider-name
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show the shipping provider name instead of its internal slug in fulfillment emails, order notes, and the orders list.
diff --git a/plugins/woocommerce/client/admin/docs/features/fulfillments-hooks.md b/plugins/woocommerce/client/admin/docs/features/fulfillments-hooks.md
index ba9e3b64d2f..b3ea6d7b19e 100644
--- a/plugins/woocommerce/client/admin/docs/features/fulfillments-hooks.md
+++ b/plugins/woocommerce/client/admin/docs/features/fulfillments-hooks.md
@@ -687,6 +687,29 @@ function add_custom_providers( $providers ) {

 **Note:** `My_Custom_Shipping_Provider` class needs to extend `Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AbstractShippingProvider` class to be able to run tracking number parsing.

+#### `woocommerce_fulfillment_shipment_provider_label`
+
+Filters the shipment provider label shown to customers and merchants (fulfillment emails, order notes, and the orders list column).
+
+**File:** `src/Admin/Features/Fulfillments/FulfillmentUtils.php`
+
+**Parameters:**
+
+-   `$label` (string) - The resolved provider label (display name for known providers, the merchant-entered name for custom "other" providers, or the slug as a last resort)
+-   `$fulfillment` (Fulfillment) - The fulfillment object
+
+**Return Value:** The provider label to display.
+
+**Purpose:** Adjust how a provider's name is displayed without overriding templates.
+
+```php
+add_filter( 'woocommerce_fulfillment_shipment_provider_label', 'customize_provider_label', 10, 2 );
+
+function customize_provider_label( $label, $fulfillment ) {
+    return 'usps' === $fulfillment->get_shipment_provider() ? 'USPS (Priority)' : $label;
+}
+```
+
 ## Best Practices

 ### Hook Priority and Execution Order
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentOrderNotes.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentOrderNotes.php
index 775cbe5a400..32355d856a2 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentOrderNotes.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentOrderNotes.php
@@ -324,7 +324,7 @@ class FulfillmentOrderNotes {
 	 */
 	private function format_tracking( Fulfillment $fulfillment ): string {
 		$tracking_number   = $fulfillment->get_tracking_number();
-		$shipping_provider = $fulfillment->get_shipment_provider();
+		$shipping_provider = FulfillmentUtils::get_provider_display_name( $fulfillment );
 		$tracking_url      = $fulfillment->get_tracking_url();

 		if ( null === $tracking_number ) {
@@ -333,7 +333,7 @@ class FulfillmentOrderNotes {

 		$parts = array( $tracking_number );

-		if ( null !== $shipping_provider ) {
+		if ( '' !== $shipping_provider ) {
 			$parts[] = sprintf(
 				/* translators: %s: shipping provider name */
 				__( 'Provider: %s', 'woocommerce' ),
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php
index bc034724254..01c0c156fe6 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php
@@ -739,4 +739,50 @@ class FulfillmentUtils {

 		return ! empty( $shipment_provider ) ? $shipment_provider : $provider_name;
 	}
+
+	/**
+	 * Resolve the human-readable provider label for display.
+	 *
+	 * Unlike {@see self::resolve_provider_name()}, which returns the stable slug for
+	 * known providers, this returns the provider's display name (e.g. "DHL" for "dhl")
+	 * so it can be shown to customers. Custom providers use the merchant-entered name,
+	 * and an unknown slug falls back to that name or the slug itself.
+	 *
+	 * @since 11.3.0
+	 *
+	 * @param Fulfillment $fulfillment The fulfillment object.
+	 *
+	 * @return string The provider label to display.
+	 */
+	public static function get_provider_display_name( Fulfillment $fulfillment ): string {
+		$provider_slug     = $fulfillment->get_shipment_provider() ?? '';
+		$provider_name_raw = $fulfillment->get_meta( '_provider_name', true );
+		$provider_name     = is_scalar( $provider_name_raw ) ? (string) $provider_name_raw : '';
+
+		// 'other' is the reserved sentinel for a merchant-entered custom provider, so its
+		// name always comes from meta, never from a provider that happens to share the key.
+		if ( 'other' === $provider_slug ) {
+			$label = '' !== $provider_name ? $provider_name : $provider_slug;
+		} else {
+			$known_providers = self::get_shipping_providers();
+			if ( isset( $known_providers[ $provider_slug ] ) ) {
+				$label = $known_providers[ $provider_slug ]->get_name();
+			} else {
+				$label = '' !== $provider_name ? $provider_name : $provider_slug;
+			}
+		}
+
+		/**
+		 * Filters the shipment provider label shown to customers.
+		 *
+		 * @since 11.3.0
+		 *
+		 * @param string      $label       The resolved provider label.
+		 * @param Fulfillment $fulfillment The fulfillment object.
+		 */
+		$filtered_label = apply_filters( 'woocommerce_fulfillment_shipment_provider_label', $label, $fulfillment );
+
+		// Keep the resolved label if a callback returns something that can't be rendered as text.
+		return is_scalar( $filtered_label ) ? (string) $filtered_label : $label;
+	}
 }
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
index 51261f7fb97..e014468bd71 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
@@ -193,13 +193,8 @@ class FulfillmentsRenderer {
 			echo '<span>' . esc_html__( 'Multiple providers', 'woocommerce' ) . '</span>';
 		} elseif ( 1 === count( $providers ) ) {
 			$provider_fulfillment   = reset( $providers );
-			$provider_slug          = $provider_fulfillment->get_shipment_provider();
-			$known_providers        = FulfillmentUtils::get_shipping_providers();
-			$provider_name_meta     = $provider_fulfillment->get_meta( '_provider_name' );
-			$provider_display_label = isset( $known_providers[ $provider_slug ] )
-				? $known_providers[ $provider_slug ]->get_name()
-				: ( ! empty( $provider_name_meta ) ? $provider_name_meta : $provider_slug );
-			echo '<span>' . esc_html( $provider_display_label ) . '</span>';
+			$provider_display_label = FulfillmentUtils::get_provider_display_name( $provider_fulfillment );
+			echo '<span>' . esc_html( '' !== $provider_display_label ? $provider_display_label : '--' ) . '</span>';
 		} else {
 			echo '<span>--</span>';
 		}
diff --git a/plugins/woocommerce/templates/emails/email-fulfillment-details.php b/plugins/woocommerce/templates/emails/email-fulfillment-details.php
index 5a6bac3a604..852e19a3458 100644
--- a/plugins/woocommerce/templates/emails/email-fulfillment-details.php
+++ b/plugins/woocommerce/templates/emails/email-fulfillment-details.php
@@ -12,9 +12,11 @@
  *
  * @see https://woocommerce.com/document/template-structure/
  * @package WooCommerce\Templates\Emails
- * @version 10.7.0
+ * @version 11.3.0
  */

+use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils;
+
 defined( 'ABSPATH' ) || exit;

 $text_align = is_rtl() ? 'right' : 'left';
@@ -31,7 +33,7 @@ if ( null === $fulfillment->get_date_deleted() ) {
 		echo '<p>' . esc_html__( 'No tracking information available for this fulfillment at the moment.', 'woocommerce' ) . '</p>';
 	} else {
 		echo '<p><strong>' . esc_html__( 'Tracking Number', 'woocommerce' ) . ':</strong> ' . esc_attr( $tracking_number ) . '</p>';
-		echo '<p><strong>' . esc_html__( 'Shipment Provider', 'woocommerce' ) . ':</strong> ' . esc_html( $shipment_provider ) . '</p>';
+		echo '<p><strong>' . esc_html__( 'Shipment Provider', 'woocommerce' ) . ':</strong> ' . esc_html( FulfillmentUtils::get_provider_display_name( $fulfillment ) ) . '</p>';
 		echo '<p><a href="' . esc_url( $tracking_url ) . '" target="_blank">' . esc_attr__( 'Track your shipment', 'woocommerce' ) . '</a></p>';
 	}
 	echo '<br />';
diff --git a/plugins/woocommerce/templates/emails/plain/email-fulfillment-details.php b/plugins/woocommerce/templates/emails/plain/email-fulfillment-details.php
index 9503073fd09..8a13dfca18c 100644
--- a/plugins/woocommerce/templates/emails/plain/email-fulfillment-details.php
+++ b/plugins/woocommerce/templates/emails/plain/email-fulfillment-details.php
@@ -12,9 +12,11 @@
  *
  * @see https://woocommerce.com/document/template-structure/
  * @package WooCommerce\Templates\Emails\Plain
- * @version 10.1.0
+ * @version 11.3.0
  */

+use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils;
+
 defined( 'ABSPATH' ) || exit;

 if ( null === $fulfillment->get_date_deleted() ) {
@@ -26,7 +28,7 @@ if ( null === $fulfillment->get_date_deleted() ) {
 		return;
 	} else {
 		echo esc_html__( 'Tracking Number', 'woocommerce' ) . ': ' . esc_attr( $tracking_number ) . "\n";
-		echo esc_html__( 'Shipment Provider', 'woocommerce' ) . ': ' . esc_html( $shipment_provider ) . "\n";
+		echo esc_html__( 'Shipment Provider', 'woocommerce' ) . ': ' . esc_html( FulfillmentUtils::get_provider_display_name( $fulfillment ) ) . "\n";
 		echo esc_html__( 'Tracking URL', 'woocommerce' ) . ': ' . esc_html( $tracking_url ) . "\n\n";
 	}

diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php
index fb6d6a7673b..d0984627f4a 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php
@@ -227,12 +227,12 @@ class FulfillmentOrderNotesTest extends \WC_Unit_Test_Case {

 		$found = false;
 		foreach ( $notes as $note ) {
-			if ( str_contains( $note->content, 'TRACK123456' ) && str_contains( $note->content, 'fedex' ) ) {
+			if ( str_contains( $note->content, 'TRACK123456' ) && str_contains( $note->content, 'FedEx' ) ) {
 				$found = true;
 				break;
 			}
 		}
-		$this->assertTrue( $found, 'Expected the created note to include the shipping provider.' );
+		$this->assertTrue( $found, 'Expected the created note to include the resolved provider name, not the raw slug.' );
 	}

 	/**
@@ -316,7 +316,7 @@ class FulfillmentOrderNotesTest extends \WC_Unit_Test_Case {
 			if (
 				str_contains( $note->content, 'updated' )
 				&& str_contains( $note->content, 'UPS999' )
-				&& str_contains( $note->content, 'ups' )
+				&& str_contains( $note->content, 'Provider: UPS' )
 				&& str_contains( $note->content, 'https://ups.com/track/UPS999' )
 			) {
 				$found = true;
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentUtilsTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentUtilsTest.php
index 9e87632abaa..fe8cf11a602 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentUtilsTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentUtilsTest.php
@@ -4,12 +4,27 @@ namespace Automattic\WooCommerce\Tests\Admin\Features\Fulfillments;

 use Automattic\WooCommerce\Admin\Features\Fulfillments\Fulfillment;
 use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils;
+use Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\USPSShippingProvider;
 use Automattic\WooCommerce\Tests\Admin\Features\Fulfillments\Helpers\FulfillmentsHelper;

 /**
  * FulfillmentUtilsTest class.
  */
 class FulfillmentUtilsTest extends \WC_Unit_Test_Case {
+
+	/**
+	 * Register a known built-in provider so the registry lookup resolves its display name,
+	 * independently of whether the feature's own providers are registered in the test run.
+	 */
+	private function register_known_provider(): void {
+		add_filter(
+			'woocommerce_fulfillment_shipping_providers',
+			function ( $providers ) {
+				$providers[] = new USPSShippingProvider();
+				return $providers;
+			}
+		);
+	}
 	/**
 	 * Original value of the fulfillments feature flag.
 	 *
@@ -168,4 +183,201 @@ class FulfillmentUtilsTest extends \WC_Unit_Test_Case {

 		$this->assertSame( '', FulfillmentUtils::resolve_provider_name( $reloaded ) );
 	}
+
+	/**
+	 * @testdox get_provider_display_name returns the registered display name for a known provider slug.
+	 */
+	public function test_get_provider_display_name_returns_registered_name_for_known_provider(): void {
+		$this->register_known_provider();
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'usps',
+				'_provider_name'     => 'ignored-for-known-providers',
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		$this->assertSame( 'USPS', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
+
+	/**
+	 * @testdox get_provider_display_name returns the merchant-entered name for custom providers.
+	 */
+	public function test_get_provider_display_name_returns_custom_name_for_other_provider(): void {
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'other',
+				'_provider_name'     => 'My Custom Carrier',
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		$this->assertSame( 'My Custom Carrier', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
+
+	/**
+	 * @testdox get_provider_display_name falls back to the slug for a custom provider with no name.
+	 */
+	public function test_get_provider_display_name_falls_back_to_slug_for_other_without_name(): void {
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'other',
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		$this->assertSame( 'other', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
+
+	/**
+	 * @testdox get_provider_display_name ignores a non-scalar provider name instead of casting it to "Array".
+	 */
+	public function test_get_provider_display_name_ignores_non_scalar_provider_name(): void {
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'other',
+				'_provider_name'     => array( 'unexpected', 'array' ),
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		$this->assertSame( 'other', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
+
+	/**
+	 * @testdox get_provider_display_name falls back to the provider name for an unknown slug.
+	 */
+	public function test_get_provider_display_name_falls_back_to_name_for_unknown_slug(): void {
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'not-a-registered-slug',
+				'_provider_name'     => 'Regional Courier',
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		$this->assertSame( 'Regional Courier', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
+
+	/**
+	 * @testdox get_provider_display_name falls back to the slug when an unknown provider has no name.
+	 */
+	public function test_get_provider_display_name_falls_back_to_slug_when_no_name(): void {
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'not-a-registered-slug',
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		$this->assertSame( 'not-a-registered-slug', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
+
+	/**
+	 * @testdox get_provider_display_name passes the resolved label through the woocommerce_fulfillment_shipment_provider_label filter.
+	 */
+	public function test_get_provider_display_name_applies_label_filter(): void {
+		$this->register_known_provider();
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'usps',
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		add_filter(
+			'woocommerce_fulfillment_shipment_provider_label',
+			static function ( $label ) {
+				return $label . ' (via API)';
+			}
+		);
+
+		$this->assertSame( 'USPS (via API)', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
+
+	/**
+	 * @testdox get_provider_display_name keeps the resolved label when the filter returns a non-scalar.
+	 */
+	public function test_get_provider_display_name_ignores_non_scalar_filter_result(): void {
+		$this->register_known_provider();
+		$fulfillment = FulfillmentsHelper::create_fulfillment(
+			array( 'entity_id' => 123 ),
+			array(
+				'_items'             => array(
+					array(
+						'item_id' => 1,
+						'qty'     => 1,
+					),
+				),
+				'_shipment_provider' => 'usps',
+			)
+		);
+
+		$reloaded = new Fulfillment( $fulfillment->get_id() );
+
+		add_filter(
+			'woocommerce_fulfillment_shipment_provider_label',
+			static function () {
+				return array( 'unexpected' );
+			}
+		);
+
+		$this->assertSame( 'USPS', FulfillmentUtils::get_provider_display_name( $reloaded ) );
+	}
 }