Commit 6355c89f4b9 for woocommerce
commit 6355c89f4b9670c110b4603ffa708370c55a61dc
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date: Wed Apr 15 22:04:51 2026 +0800
Fix: Make customer history tooltip dynamic based on excluded statuses (#64036)
* fix: make customer history tooltip dynamic based on excluded statuses
The "Total orders" tooltip in the customer history metabox was hardcoded
to say "non-cancelled, non-failed" but the actual defaults also exclude
pending, and the list is configurable via the
woocommerce_analytics_excluded_order_statuses filter.
Extract get_excluded_statuses() from get_excluded_statuses_sql() to
avoid logic duplication, then build the tooltip dynamically by mapping
excluded slugs to their translated labels via wc_get_order_statuses()
and formatting with wp_sprintf_l() for locale-aware list output.
* Add changefile(s) from automation for the following project(s): woocommerce
* fix: remove inaccurate "including the current one" from tooltip
The current order is only included in the count when its status is not
excluded, so the phrase was misleading for excluded-status orders.
* fix: improve tooltip clarity and test coverage for customer history
- Remove redundant sanitize_title() on already-clean slugs
- Add comment explaining auto-draft/trash filtering intent
- Improve translators comment for localized list placeholder
- Add test for empty-excluded-statuses fallback tooltip
* fix: use mb_strtolower() for multibyte-safe lowercase in tooltip
* Exclude draft, and make sure excluded_order_statuses filter is still called once
* Add changefile(s) from automation for the following project(s): woocommerce
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
Co-authored-by: Lourens Schep <lourensschep@gmail.com>
diff --git a/plugins/woocommerce/changelog/64036-fix-dynamic-customer-history-tooltip b/plugins/woocommerce/changelog/64036-fix-dynamic-customer-history-tooltip
new file mode 100644
index 00000000000..373d0ee2787
--- /dev/null
+++ b/plugins/woocommerce/changelog/64036-fix-dynamic-customer-history-tooltip
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix customer history "Total orders" tooltip to dynamically reflect excluded order statuses instead of using hardcoded text.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php
index 93a9b7c8b89..1d8e1aa7798 100644
--- a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php
+++ b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php
@@ -14,6 +14,13 @@ use WC_Order;
*/
class CustomerHistory {
+ /**
+ * Memoized excluded statuses to avoid redundant option reads and filter calls per request.
+ *
+ * @var string[]|null
+ */
+ private $excluded_statuses = null;
+
/**
* Output the customer history template for the order.
*
@@ -37,7 +44,7 @@ class CustomerHistory {
*
* @param WC_Order $order The order object.
*
- * @return array{orders_count: int, total_spend: float, avg_order_value: float} Order count, total spend, and average order value.
+ * @return array{orders_count: int, total_spend: float, avg_order_value: float, tooltip: string} Order count, total spend, average order value, and tooltip text.
*/
private function get_customer_history( WC_Order $order ): array {
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
@@ -60,10 +67,38 @@ class CustomerHistory {
$orders_count = (int) ( $result->orders_count ?? 0 );
$total_spend = (float) ( $result->total_spend ?? 0 );
+ // Build a dynamic tooltip listing the excluded statuses by their translated labels.
+ // Internal statuses (auto-draft, trash) are naturally filtered out because they
+ // don't exist in wc_get_order_statuses(). checkout-draft is skipped explicitly
+ // because it is force-excluded by DraftOrders but is not a configurable option
+ // on the Analytics settings page, so it would be confusing to surface it here.
+ $all_statuses = wc_get_order_statuses();
+ $excluded_labels = array();
+ foreach ( $this->get_excluded_statuses() as $slug ) {
+ if ( 'checkout-draft' === $slug ) {
+ continue;
+ }
+ $prefixed = 'wc-' . $slug;
+ if ( isset( $all_statuses[ $prefixed ] ) ) {
+ $excluded_labels[] = mb_strtolower( $all_statuses[ $prefixed ] );
+ }
+ }
+
+ if ( ! empty( $excluded_labels ) ) {
+ $tooltip = sprintf(
+ /* translators: %s: localized list of order status names, e.g. "pending payment, failed, and cancelled" */
+ __( 'Total number of orders for this customer, excluding %s orders, including the current one.', 'woocommerce' ),
+ wp_sprintf_l( '%l', $excluded_labels )
+ );
+ } else {
+ $tooltip = __( 'Total number of orders for this customer, including the current one.', 'woocommerce' );
+ }
+
return array(
'orders_count' => $orders_count,
'total_spend' => $total_spend,
'avg_order_value' => $orders_count > 0 ? $total_spend / $orders_count : 0,
+ 'tooltip' => $tooltip,
);
}
@@ -192,12 +227,14 @@ class CustomerHistory {
}
/**
- * Get the SQL fragment for excluded order statuses.
+ * Get the list of excluded order statuses for customer history.
*
- * @return string SQL IN clause, e.g. ( 'auto-draft','trash','wc-pending','wc-failed',... ), or empty string if no statuses are excluded.
+ * @return string[] Excluded status slugs without wc- prefix (e.g. 'auto-draft', 'trash', 'pending', 'failed', 'cancelled').
*/
- private function get_excluded_statuses_sql(): string {
- global $wpdb;
+ private function get_excluded_statuses(): array {
+ if ( null !== $this->excluded_statuses ) {
+ return $this->excluded_statuses;
+ }
$excluded_statuses = get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) );
if ( ! is_array( $excluded_statuses ) ) {
@@ -216,6 +253,20 @@ class CustomerHistory {
$excluded_statuses = array( 'auto-draft', 'trash', 'pending', 'failed', 'cancelled' );
}
+ $this->excluded_statuses = $excluded_statuses;
+ return $this->excluded_statuses;
+ }
+
+ /**
+ * Get the SQL fragment for excluded order statuses.
+ *
+ * @return string SQL IN clause, e.g. ( 'auto-draft','trash','wc-pending','wc-failed',... ), or empty string if no statuses are excluded.
+ */
+ private function get_excluded_statuses_sql(): string {
+ global $wpdb;
+
+ $excluded_statuses = $this->get_excluded_statuses();
+
if ( empty( $excluded_statuses ) ) {
return '';
}
diff --git a/plugins/woocommerce/templates/order/customer-history.php b/plugins/woocommerce/templates/order/customer-history.php
index 62a3c5aa2e6..7532b200be7 100644
--- a/plugins/woocommerce/templates/order/customer-history.php
+++ b/plugins/woocommerce/templates/order/customer-history.php
@@ -6,7 +6,7 @@
*
* @see Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomerHistory
* @package WooCommerce\Templates
- * @version 8.7.0
+ * @version 10.8.0
*/
declare( strict_types=1 );
@@ -16,9 +16,10 @@ defined( 'ABSPATH' ) || exit;
/**
* Variables used in this file.
*
- * @var int $orders_count The number of paid orders placed by the current customer.
- * @var float $total_spend The total money spent by the current customer.
- * @var float $avg_order_value The average money spent by the current customer.
+ * @var int $orders_count The number of paid orders placed by the current customer.
+ * @var float $total_spend The total money spent by the current customer.
+ * @var float $avg_order_value The average money spent by the current customer.
+ * @var string $tooltip The tooltip text for the "Total orders" heading.
*/
?>
@@ -26,11 +27,7 @@ defined( 'ABSPATH' ) || exit;
<h4>
<?php
esc_html_e( 'Total orders', 'woocommerce' );
- echo wp_kses_post(
- wc_help_tip(
- __( 'Total number of non-cancelled, non-failed orders for this customer, including the current one.', 'woocommerce' )
- )
- );
+ echo wp_kses_post( wc_help_tip( $tooltip ) );
?>
</h4>
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
index 466525d307d..e43bb7f8ad5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
@@ -364,6 +364,150 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
$this->assertMatchesRegularExpression( '/order-attribution-total-spend">\s*.*200\.00/', $output_b, 'Guest B should see total spend of 200' );
}
+ /**
+ * @testdox Tooltip should list default excluded statuses (pending payment, failed, cancelled).
+ */
+ public function test_tooltip_shows_default_excluded_statuses(): void {
+ $this->toggle_cot_feature_and_usage( true );
+
+ $customer_id = $this->factory->user->create();
+
+ $order = WC_Helper_Order::create_order( $customer_id );
+ $order->set_status( 'completed' );
+ $order->save();
+
+ ob_start();
+ $this->sut->output( $order );
+ $output = ob_get_clean();
+
+ $this->assertStringContainsString( 'pending payment', $output, 'Tooltip should mention "pending payment"' );
+ $this->assertStringContainsString( 'failed', $output, 'Tooltip should mention "failed"' );
+ $this->assertStringContainsString( 'cancelled', $output, 'Tooltip should mention "cancelled"' );
+ }
+
+ /**
+ * @testdox Tooltip should reflect custom excluded statuses option.
+ */
+ public function test_tooltip_reflects_custom_option(): void {
+ $this->toggle_cot_feature_and_usage( true );
+
+ update_option( 'woocommerce_excluded_report_order_statuses', array( 'cancelled' ) );
+
+ $customer_id = $this->factory->user->create();
+
+ $order = WC_Helper_Order::create_order( $customer_id );
+ $order->set_status( 'completed' );
+ $order->save();
+
+ ob_start();
+ $this->sut->output( $order );
+ $output = ob_get_clean();
+
+ delete_option( 'woocommerce_excluded_report_order_statuses' );
+
+ $this->assertStringContainsString( 'cancelled', $output, 'Tooltip should mention "cancelled"' );
+ $this->assertStringNotContainsString( 'pending payment', $output, 'Tooltip should not mention "pending payment"' );
+ $this->assertStringNotContainsString( 'failed', $output, 'Tooltip should not mention "failed"' );
+ }
+
+ /**
+ * @testdox Tooltip should reflect statuses added via filter.
+ */
+ public function test_tooltip_reflects_filter(): void {
+ $this->toggle_cot_feature_and_usage( true );
+
+ $add_on_hold = function ( $statuses ) {
+ $statuses[] = 'on-hold';
+ return $statuses;
+ };
+ add_filter( 'woocommerce_analytics_excluded_order_statuses', $add_on_hold );
+
+ $customer_id = $this->factory->user->create();
+
+ $order = WC_Helper_Order::create_order( $customer_id );
+ $order->set_status( 'completed' );
+ $order->save();
+
+ ob_start();
+ $this->sut->output( $order );
+ $output = ob_get_clean();
+
+ remove_filter( 'woocommerce_analytics_excluded_order_statuses', $add_on_hold );
+
+ $this->assertStringContainsString( 'on hold', $output, 'Tooltip should mention "on hold"' );
+ }
+
+ /**
+ * @testdox Tooltip should not display internal statuses like auto-draft, trash, or checkout-draft.
+ */
+ public function test_tooltip_excludes_internal_statuses(): void {
+ $this->toggle_cot_feature_and_usage( true );
+
+ $customer_id = $this->factory->user->create();
+
+ $order = WC_Helper_Order::create_order( $customer_id );
+ $order->set_status( 'completed' );
+ $order->save();
+
+ ob_start();
+ $this->sut->output( $order );
+ $output = ob_get_clean();
+
+ $this->assertStringNotContainsString( 'auto-draft', $output, 'Tooltip should not mention "auto-draft"' );
+ $this->assertStringNotContainsString( 'trash', $output, 'Tooltip should not mention "trash"' );
+ }
+
+ /**
+ * @testdox Tooltip should not display checkout-draft even when it is added via filter.
+ */
+ public function test_tooltip_excludes_checkout_draft_status(): void {
+ $this->toggle_cot_feature_and_usage( true );
+
+ $add_checkout_draft = function ( $statuses ) {
+ $statuses[] = 'checkout-draft';
+ return $statuses;
+ };
+ add_filter( 'woocommerce_analytics_excluded_order_statuses', $add_checkout_draft );
+
+ $customer_id = $this->factory->user->create();
+
+ $order = WC_Helper_Order::create_order( $customer_id );
+ $order->set_status( 'completed' );
+ $order->save();
+
+ ob_start();
+ $this->sut->output( $order );
+ $output = ob_get_clean();
+
+ remove_filter( 'woocommerce_analytics_excluded_order_statuses', $add_checkout_draft );
+
+ $this->assertStringNotContainsString( 'draft', $output, 'Tooltip should not mention "draft" for checkout-draft status' );
+ }
+
+ /**
+ * @testdox Tooltip should show generic message when all statuses are removed from exclusion.
+ */
+ public function test_tooltip_shows_no_exclusion_message_when_all_statuses_removed(): void {
+ $this->toggle_cot_feature_and_usage( true );
+
+ add_filter( 'woocommerce_analytics_excluded_order_statuses', '__return_empty_array' );
+
+ $customer_id = $this->factory->user->create();
+
+ $order = WC_Helper_Order::create_order( $customer_id );
+ $order->set_status( 'completed' );
+ $order->save();
+
+ ob_start();
+ $this->sut->output( $order );
+ $output = ob_get_clean();
+
+ remove_filter( 'woocommerce_analytics_excluded_order_statuses', '__return_empty_array' );
+
+ $this->assertStringContainsString( 'Total number of orders for this customer, including the current one.', $output, 'Tooltip should use the no-exclusions fallback string' );
+ $this->assertStringNotContainsString( 'excluding', $output, 'Tooltip should not mention "excluding"' );
+ }
+
/**
* @testdox CPT fallback should render correct customer history from analytics tables.
*/