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 ) );
+ }
}