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.
 	 */