Commit a0fc71ed0d3 for woocommerce

commit a0fc71ed0d305b9f64b4e175498d845341a25de2
Author: Povilas Staskus <povilas.staskus@automattic.com>
Date:   Tue Mar 17 15:03:47 2026 +0100

    Make template_id optional in send_email order action with server-side auto-selection (#63556)

    * Make template_id optional and add server-side auto-selection in send_email

    When template_id is omitted from the send_email order action endpoint,
    the server auto-selects the appropriate email template based on order
    status using a shared STATUS_TEMPLATE_MAP constant. POS email classes
    self-register their priority via the existing filter pattern, keeping
    the controller POS-agnostic.

    * Update template_id REST param description to clarify optional auto-selection

    * Sanitize priority filter output in select_default_template

    * Rename priority to preferred_template_ids for clarity across filter, method, and variables

    * Fix add_order_note parameter type (false to 0) and remove baseline entry

    * Simplify select_default_template logic and rename POS method for consistency

    * Restrict POS email templates to matching order statuses

    POS completed and refunded email templates were available and preferred
    for any POS order regardless of status. A failed POS order would
    auto-select customer_pos_completed_order instead of customer_failed_order.

    Extract is_applicable_for_order() in each POS email class to check both
    POS origin and the correct status/refund condition before adding the
    template to valid or preferred lists.

    * Split POS refunded email availability vs preference conditions

    Partially refunded POS orders (status=completed) should auto-select
    the POS completed template, not POS refunded. Split the shared
    is_applicable_for_order() into is_available_for_order() (status=refunded
    OR has refunds) and is_preferred_for_order() (status=refunded only).

diff --git a/plugins/woocommerce/changelog/woomob-2403-pos-bookings-emails-send-a-correct-email-if-booking-was-paid b/plugins/woocommerce/changelog/woomob-2403-pos-bookings-emails-send-a-correct-email-if-booking-was-paid
new file mode 100644
index 00000000000..50c7ca48d49
--- /dev/null
+++ b/plugins/woocommerce/changelog/woomob-2403-pos-bookings-emails-send-a-correct-email-if-booking-was-paid
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add server-side auto-selection of email template when template_id is omitted in send_email endpoint.
diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-completed-order.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-completed-order.php
index ea11b1df635..e9f38834385 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-completed-order.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-completed-order.php
@@ -9,6 +9,7 @@ if ( ! defined( 'ABSPATH' ) ) {
 	exit; // Exit if accessed directly.
 }

+use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Internal\Email\OrderPriceFormatter;
 use Automattic\WooCommerce\Internal\Orders\PointOfSaleOrderUtil;
 use Automattic\WooCommerce\Internal\Settings\PointOfSaleDefaultSettings;
@@ -383,6 +384,7 @@ if ( ! class_exists( 'WC_Email_Customer_POS_Completed_Order', false ) ) :
 		 */
 		private function enable_email_template_for_pos_orders() {
 			add_filter( 'woocommerce_rest_order_actions_email_valid_template_classes', array( $this, 'add_to_valid_template_classes' ), 10, 2 );
+			add_filter( 'woocommerce_rest_order_actions_email_preferred_template_ids', array( $this, 'add_to_preferred_template_ids' ), 10, 2 );
 		}

 		/**
@@ -396,13 +398,43 @@ if ( ! class_exists( 'WC_Email_Customer_POS_Completed_Order', false ) ) :
 		 * @since 10.6.0
 		 */
 		public function add_to_valid_template_classes( $valid_template_classes, $order ) {
-			if ( ! PointOfSaleOrderUtil::is_order_paid_at_pos( $order ) ) {
+			if ( ! $this->is_applicable_for_order( $order ) ) {
 				return $valid_template_classes;
 			}
 			$valid_template_classes[] = get_class( $this );
 			return $valid_template_classes;
 		}

+		/**
+		 * Prepend this template to the preferred template IDs for POS-paid orders.
+		 *
+		 * @internal For exclusive usage within this class, backwards compatibility not guaranteed.
+		 *
+		 * @param array    $preferred_template_ids Ordered array of template IDs.
+		 * @param WC_Order $order                  The order.
+		 * @return array Modified array of template IDs.
+		 *
+		 * @since 10.7.0
+		 */
+		public function add_to_preferred_template_ids( $preferred_template_ids, $order ) {
+			if ( ! $this->is_applicable_for_order( $order ) ) {
+				return $preferred_template_ids;
+			}
+			array_unshift( $preferred_template_ids, $this->id );
+			return $preferred_template_ids;
+		}
+
+		/**
+		 * Check if this email template is applicable for the given order.
+		 *
+		 * @param WC_Order $order The order.
+		 * @return bool
+		 */
+		private function is_applicable_for_order( $order ): bool {
+			return PointOfSaleOrderUtil::is_order_paid_at_pos( $order )
+				&& OrderStatus::COMPLETED === $order->get_status( 'edit' );
+		}
+
 		/**
 		 * Get the store name from POS settings.
 		 *
diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-refunded-order.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-refunded-order.php
index 34edc84ee46..9df5da7b226 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-refunded-order.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-pos-refunded-order.php
@@ -5,6 +5,7 @@
  * @package WooCommerce\Emails
  */

+use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Internal\Email\OrderPriceFormatter;
 use Automattic\WooCommerce\Internal\Orders\PointOfSaleOrderUtil;
 use Automattic\WooCommerce\Internal\Settings\PointOfSaleDefaultSettings;
@@ -455,6 +456,7 @@ if ( ! class_exists( 'WC_Email_Customer_POS_Refunded_Order', false ) ) :
 			add_action( 'woocommerce_order_fully_refunded_notification', array( $this, 'auto_trigger' ), 10, 2 );
 			add_action( 'woocommerce_order_partially_refunded_notification', array( $this, 'auto_trigger' ), 10, 2 );
 			add_filter( 'woocommerce_rest_order_actions_email_valid_template_classes', array( $this, 'add_to_valid_template_classes' ), 10, 2 );
+			add_filter( 'woocommerce_rest_order_actions_email_preferred_template_ids', array( $this, 'add_to_preferred_template_ids' ), 10, 2 );
 			add_action( 'woocommerce_rest_order_actions_email_send', array( $this, 'trigger' ), 10, 2 );
 		}

@@ -469,16 +471,54 @@ if ( ! class_exists( 'WC_Email_Customer_POS_Refunded_Order', false ) ) :
 		 * @since 10.6.0
 		 */
 		public function add_to_valid_template_classes( $valid_template_classes, $order ) {
-			if ( 0 === count( $order->get_refunds() ) ) {
-				return $valid_template_classes;
-			}
-			if ( ! PointOfSaleOrderUtil::is_order_paid_at_pos( $order ) ) {
+			if ( ! $this->is_available_for_order( $order ) ) {
 				return $valid_template_classes;
 			}
 			$valid_template_classes[] = get_class( $this );
 			return $valid_template_classes;
 		}

+		/**
+		 * Prepend this template to the preferred template IDs for POS-paid orders.
+		 *
+		 * @internal For exclusive usage within this class, backwards compatibility not guaranteed.
+		 *
+		 * @param array    $preferred_template_ids Ordered array of template IDs.
+		 * @param WC_Order $order                  The order.
+		 * @return array Modified array of template IDs.
+		 *
+		 * @since 10.7.0
+		 */
+		public function add_to_preferred_template_ids( $preferred_template_ids, $order ) {
+			if ( ! $this->is_preferred_for_order( $order ) ) {
+				return $preferred_template_ids;
+			}
+			array_unshift( $preferred_template_ids, $this->id );
+			return $preferred_template_ids;
+		}
+
+		/**
+		 * Check if this email template is available (can be manually selected) for the given order.
+		 *
+		 * @param WC_Order $order The order.
+		 * @return bool
+		 */
+		private function is_available_for_order( $order ): bool {
+			return PointOfSaleOrderUtil::is_order_paid_at_pos( $order )
+				&& ( OrderStatus::REFUNDED === $order->get_status( 'edit' ) || 0 !== count( $order->get_refunds() ) );
+		}
+
+		/**
+		 * Check if this email template is preferred (auto-selected) for the given order.
+		 *
+		 * @param WC_Order $order The order.
+		 * @return bool
+		 */
+		private function is_preferred_for_order( $order ): bool {
+			return PointOfSaleOrderUtil::is_order_paid_at_pos( $order )
+				&& OrderStatus::REFUNDED === $order->get_status( 'edit' );
+		}
+
 		/**
 		 * Add additional details to the order item totals table.
 		 *
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 6ffe4954fe8..eaa61ed32f1 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -65895,19 +65895,13 @@ parameters:
 		-
 			message: '#^Cannot call method add_order_note\(\) on WC_Order\|WC_Order_Refund\|false\.$#'
 			identifier: method.nonObject
-			count: 2
+			count: 1
 			path: src/Internal/Orders/OrderActionsRestController.php

 		-
 			message: '#^Cannot call method get_billing_email\(\) on WC_Order\|WC_Order_Refund\|false\.$#'
 			identifier: method.nonObject
-			count: 4
-			path: src/Internal/Orders/OrderActionsRestController.php
-
-		-
-			message: '#^Cannot call method get_id\(\) on WC_Order\|WC_Order_Refund\|false\.$#'
-			identifier: method.nonObject
-			count: 7
+			count: 2
 			path: src/Internal/Orders/OrderActionsRestController.php

 		-
@@ -65967,18 +65961,12 @@ parameters:
 		-
 			message: '#^Parameter \#1 \$order of method Automattic\\WooCommerce\\Internal\\Orders\\OrderActionsRestController\:\:get_available_email_templates\(\) expects WC_Order, WC_Order\|WC_Order_Refund\|false given\.$#'
 			identifier: argument.type
-			count: 2
+			count: 1
 			path: src/Internal/Orders/OrderActionsRestController.php

 		-
 			message: '#^Parameter \#1 \$order of method Automattic\\WooCommerce\\Internal\\Orders\\OrderActionsRestController\:\:maybe_update_billing_email\(\) expects WC_Order, WC_Order\|WC_Order_Refund\|false given\.$#'
 			identifier: argument.type
-			count: 2
-			path: src/Internal/Orders/OrderActionsRestController.php
-
-		-
-			message: '#^Parameter \#1 \$order of method Automattic\\WooCommerce\\Internal\\Orders\\OrderActionsRestController\:\:order_is_partially_refunded\(\) expects WC_Order, WC_Order\|WC_Order_Refund\|false given\.$#'
-			identifier: argument.type
 			count: 1
 			path: src/Internal/Orders/OrderActionsRestController.php

diff --git a/plugins/woocommerce/src/Internal/Orders/OrderActionsRestController.php b/plugins/woocommerce/src/Internal/Orders/OrderActionsRestController.php
index 93d2639b315..fbcf527f878 100644
--- a/plugins/woocommerce/src/Internal/Orders/OrderActionsRestController.php
+++ b/plugins/woocommerce/src/Internal/Orders/OrderActionsRestController.php
@@ -18,6 +18,32 @@ use WP_REST_Request, WP_REST_Response, WP_REST_Server;
  * This first version only supports sending the order details to the customer (`send_order_details`).
  */
 class OrderActionsRestController extends RestApiControllerBase {
+	/**
+	 * Maps order statuses to their corresponding email template class and ID.
+	 */
+	private const STATUS_TEMPLATE_MAP = array(
+		OrderStatus::COMPLETED  => array(
+			'class' => 'WC_Email_Customer_Completed_Order',
+			'id'    => 'customer_completed_order',
+		),
+		OrderStatus::FAILED     => array(
+			'class' => 'WC_Email_Customer_Failed_Order',
+			'id'    => 'customer_failed_order',
+		),
+		OrderStatus::ON_HOLD    => array(
+			'class' => 'WC_Email_Customer_On_Hold_Order',
+			'id'    => 'customer_on_hold_order',
+		),
+		OrderStatus::PROCESSING => array(
+			'class' => 'WC_Email_Customer_Processing_Order',
+			'id'    => 'customer_processing_order',
+		),
+		OrderStatus::REFUNDED   => array(
+			'class' => 'WC_Email_Customer_Refunded_Order',
+			'id'    => 'customer_refunded_order',
+		),
+	);
+
 	/**
 	 * Get the WooCommerce REST API namespace for the class.
 	 *
@@ -173,11 +199,11 @@ class OrderActionsRestController extends RestApiControllerBase {

 		if ( 'send_email' === $action_slug ) {
 			$args['template_id'] = array(
-				'description'       => __( 'The ID of the template to use for sending the email.', 'woocommerce' ),
+				'description'       => __( 'The email template to use. If omitted, the best template is auto-selected based on order status.', 'woocommerce' ),
 				'type'              => 'string',
 				'enum'              => $this->get_template_id_enum(),
 				'context'           => array( 'edit' ),
-				'required'          => true,
+				'required'          => false,
 				'validate_callback' => 'rest_validate_request_arg',
 			);
 		}
@@ -298,22 +324,8 @@ class OrderActionsRestController extends RestApiControllerBase {
 			$valid_template_classes[] = 'WC_Email_Customer_Refunded_Order';
 		}

-		switch ( $order_status ) {
-			case OrderStatus::COMPLETED:
-				$valid_template_classes[] = 'WC_Email_Customer_Completed_Order';
-				break;
-			case OrderStatus::FAILED:
-				$valid_template_classes[] = 'WC_Email_Customer_Failed_Order';
-				break;
-			case OrderStatus::ON_HOLD:
-				$valid_template_classes[] = 'WC_Email_Customer_On_Hold_Order';
-				break;
-			case OrderStatus::PROCESSING:
-				$valid_template_classes[] = 'WC_Email_Customer_Processing_Order';
-				break;
-			case OrderStatus::REFUNDED:
-				$valid_template_classes[] = 'WC_Email_Customer_Refunded_Order';
-				break;
+		if ( isset( self::STATUS_TEMPLATE_MAP[ $order_status ] ) ) {
+			$valid_template_classes[] = self::STATUS_TEMPLATE_MAP[ $order_status ]['class'];
 		}

 		/**
@@ -368,6 +380,80 @@ class OrderActionsRestController extends RestApiControllerBase {
 		return reset( $matching_templates );
 	}

+	/**
+	 * Select the best email template from available templates using priority ordering.
+	 *
+	 * @param WC_Order   $order               The order.
+	 * @param WC_Email[] $available_templates  Already-filtered available templates.
+	 *
+	 * @return WC_Email|null
+	 */
+	private function select_default_template( WC_Order $order, array $available_templates ): ?WC_Email {
+		if ( empty( $available_templates ) ) {
+			return null;
+		}
+
+		$default_preferred_ids = $this->get_default_preferred_template_ids( $order );
+
+		/**
+		 * Filter the preferred template IDs for auto-selecting an email template.
+		 *
+		 * Template IDs earlier in the array are preferred. Only templates that
+		 * are also in the available templates list will be considered.
+		 *
+		 * @since 10.7.0
+		 *
+		 * @param string[] $preferred_template_ids Ordered array of template IDs (most preferred first).
+		 * @param WC_Order $order                  The order.
+		 * @param string[] $available              The available template IDs for this order.
+		 */
+		$preferred_template_ids = apply_filters(
+			'woocommerce_rest_order_actions_email_preferred_template_ids',
+			$default_preferred_ids,
+			$order,
+			array_map( fn( $t ) => $t->id, $available_templates )
+		);
+
+		if ( ! is_array( $preferred_template_ids ) ) {
+			$preferred_template_ids = $default_preferred_ids;
+		}
+
+		$preferred_template_ids = array_filter( array_unique( $preferred_template_ids ), 'is_string' );
+
+		foreach ( $preferred_template_ids as $candidate_id ) {
+			$template = $this->get_email_template_by_id( $candidate_id, $available_templates );
+			if ( $template ) {
+				return $template;
+			}
+		}
+
+		// Last resort: first available template.
+		$first = reset( $available_templates );
+		return $first ? $first : null;
+	}
+
+	/**
+	 * Get the default preferred template IDs for auto-selection based on order status.
+	 *
+	 * @param WC_Order $order The order.
+	 *
+	 * @return string[]
+	 */
+	private function get_default_preferred_template_ids( WC_Order $order ): array {
+		$status                 = $order->get_status( 'edit' );
+		$preferred_template_ids = array();
+
+		// Status-specific template.
+		if ( isset( self::STATUS_TEMPLATE_MAP[ $status ] ) ) {
+			$preferred_template_ids[] = self::STATUS_TEMPLATE_MAP[ $status ]['id'];
+		}
+
+		// Generic fallback.
+		$preferred_template_ids[] = 'customer_invoice';
+
+		return $preferred_template_ids;
+	}
+
 	/**
 	 * Callback to run for GET wc/v3/orders/(?P<id>[\d]+)/actions/email_templates.
 	 *
@@ -414,7 +500,12 @@ class OrderActionsRestController extends RestApiControllerBase {
 	 * @return array|WP_Error
 	 */
 	protected function send_email( WP_REST_Request $request ) {
-		$order       = wc_get_order( $request->get_param( 'id' ) );
+		$order = wc_get_order( $request->get_param( 'id' ) );
+
+		if ( ! $order instanceof WC_Order ) {
+			return new WP_Error( 'woocommerce_rest_not_found', __( 'Order not found.', 'woocommerce' ), array( 'status' => 404 ) );
+		}
+
 		$email       = $request->get_param( 'email' );
 		$force       = wp_validate_boolean( $request->get_param( 'force_email_update' ) );
 		$template_id = $request->get_param( 'template_id' );
@@ -437,18 +528,33 @@ class OrderActionsRestController extends RestApiControllerBase {
 		}

 		$available_templates = $this->get_available_email_templates( $order );
-		$template            = $this->get_email_template_by_id( $template_id, $available_templates );

-		if ( is_null( $template ) ) {
-			return new WP_Error(
-				'woocommerce_rest_invalid_email_template',
-				sprintf(
-					// translators: %s is a string ID for an email template.
-					__( '%s is not a valid template for this order.', 'woocommerce' ),
-					esc_html( $template_id )
-				),
-				array( 'status' => 400 )
-			);
+		if ( empty( $template_id ) ) {
+			$template = $this->select_default_template( $order, $available_templates );
+
+			if ( is_null( $template ) ) {
+				return new WP_Error(
+					'woocommerce_rest_no_email_template',
+					__( 'No email template is available for this order.', 'woocommerce' ),
+					array( 'status' => 400 )
+				);
+			}
+
+			$template_id = $template->id;
+		} else {
+			$template = $this->get_email_template_by_id( $template_id, $available_templates );
+
+			if ( is_null( $template ) ) {
+				return new WP_Error(
+					'woocommerce_rest_invalid_email_template',
+					sprintf(
+						// translators: %s is a string ID for an email template.
+						__( '%s is not a valid template for this order.', 'woocommerce' ),
+						esc_html( $template_id )
+					),
+					array( 'status' => 400 )
+				);
+			}
 		}

 		switch ( $template_id ) {
@@ -512,7 +618,7 @@ class OrderActionsRestController extends RestApiControllerBase {
 		foreach ( $messages as $message ) {
 			$order->add_order_note(
 				$message,
-				false,
+				0,
 				true,
 				array(
 					'user_agent' => $user_agent ? $user_agent : 'REST API',
@@ -573,7 +679,7 @@ class OrderActionsRestController extends RestApiControllerBase {
 		foreach ( $messages as $message ) {
 			$order->add_order_note(
 				$message,
-				false,
+				0,
 				true,
 				array(
 					'user_agent' => $user_agent ? $user_agent : 'REST API',
diff --git a/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php
index 0ac13ba9301..100fe201b67 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php
@@ -35,6 +35,14 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {

 		$this->user['shop_manager'] = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
 		$this->user['customer']     = $this->factory->user->create( array( 'role' => 'customer' ) );
+
+		// Load and instantiate POS email classes to register their filter hooks.
+		// WP_UnitTestCase restores hooks between tests, so this must run each setUp().
+		$bootstrap = \WC_Unit_Tests_Bootstrap::instance();
+		require_once $bootstrap->plugin_dir . '/includes/emails/class-wc-email-customer-pos-completed-order.php';
+		require_once $bootstrap->plugin_dir . '/includes/emails/class-wc-email-customer-pos-refunded-order.php';
+		new \WC_Email_Customer_POS_Completed_Order();
+		new \WC_Email_Customer_POS_Refunded_Order();
 	}

 	/**
@@ -61,6 +69,23 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
 		);
 	}

+	/**
+	 * Create a full refund for an order.
+	 *
+	 * @param \WC_Order $order The order to create a refund for.
+	 *
+	 * @return void
+	 * @throws \Exception Throws Exception if refund creation fails.
+	 */
+	private function do_full_refund( \WC_Order $order ): void {
+		wc_create_refund(
+			array(
+				'order_id' => $order->get_id(),
+				'amount'   => $order->get_total(),
+			)
+		);
+	}
+
 	/**
 	 * Data provider for `test_email_templates`.
 	 *
@@ -302,10 +327,269 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
 				),
 			),
 		);
+		yield 'auto-select completed order' => array(
+			'shop_manager',
+			array(),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Completed order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Completed order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select refunded order' => array(
+			'shop_manager',
+			array(
+				'status'      => 'refunded',
+				'full_refund' => true,
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Refunded order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Refunded order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select partially refunded order' => array(
+			'shop_manager',
+			array(
+				'partial_refund' => true,
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Completed order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Completed order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select failed order' => array(
+			'shop_manager',
+			array(
+				'status' => 'failed',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Failed order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Failed order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select on-hold order' => array(
+			'shop_manager',
+			array(
+				'status' => 'on-hold',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Order on-hold&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Order on-hold&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select processing order' => array(
+			'shop_manager',
+			array(
+				'status' => 'processing',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Processing order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Processing order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select pending order gets invoice' => array(
+			'shop_manager',
+			array(
+				'status' => 'pending',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Order details sent to customer@example.org.',
+				'notes'   => array(
+					'Order details sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select POS failed order' => array(
+			'shop_manager',
+			array(
+				'created_via' => 'pos-rest-api',
+				'status'      => 'failed',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Failed order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Failed order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select POS on-hold order' => array(
+			'shop_manager',
+			array(
+				'created_via' => 'pos-rest-api',
+				'status'      => 'on-hold',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;Order on-hold&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;Order on-hold&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select POS completed order' => array(
+			'shop_manager',
+			array(
+				'created_via' => 'pos-rest-api',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;POS completed order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;POS completed order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select POS refunded order without refund records' => array(
+			'shop_manager',
+			array(
+				'created_via' => 'pos-rest-api',
+				'status'      => 'refunded',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;POS refunded order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;POS refunded order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select POS refunded order' => array(
+			'shop_manager',
+			array(
+				'created_via' => 'pos-rest-api',
+				'status'      => 'refunded',
+				'full_refund' => true,
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;POS refunded order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;POS refunded order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select POS partially refunded order' => array(
+			'shop_manager',
+			array(
+				'created_via'    => 'pos-rest-api',
+				'partial_refund' => true,
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Email template &quot;POS completed order&quot; sent to customer@example.org.',
+				'notes'   => array(
+					'Email template &quot;POS completed order&quot; sent to customer@example.org.',
+				),
+			),
+		);
+		yield 'auto-select no billing email' => array(
+			'shop_manager',
+			array(
+				'billing_email' => '',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 400,
+				'message' => 'Order does not have an email address.',
+				'notes'   => array(),
+			),
+		);
+		yield 'auto-select auto-draft order' => array(
+			'shop_manager',
+			array(
+				'status' => 'auto-draft',
+			),
+			array(
+				'template_id' => '',
+			),
+			array(
+				'status'  => 400,
+				'message' => 'No email template is available for this order.',
+				'notes'   => array(),
+			),
+		);
+		yield 'auto-select with email param' => array(
+			'shop_manager',
+			array(
+				'billing_email' => '',
+			),
+			array(
+				'template_id' => '',
+				'email'       => 'another@example.org',
+			),
+			array(
+				'status'  => 200,
+				'message' => 'Billing email updated to another@example.org. Email template &quot;Completed order&quot; sent to another@example.org.',
+				'notes'   => array(
+					'Billing email updated to another@example.org.',
+					'Email template &quot;Completed order&quot; sent to another@example.org.',
+				),
+			),
+		);
 	}

 	/**
-	 * Test the wc/v3/orders/{id}/actions/send_email endpoint.
+	 * @testdox Test the wc/v3/orders/{id}/actions/send_email endpoint.
 	 *
 	 * @dataProvider provide_data_for_send_email
 	 *
@@ -321,6 +605,8 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
 			'billing_email'  => 'customer@example.org',
 			'status'         => 'completed',
 			'partial_refund' => false,
+			'full_refund'    => false,
+			'created_via'    => null,
 		);
 		$order_props    = wp_parse_args( $order_props, $order_defaults );

@@ -336,8 +622,15 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
 			$order = WC_Helper_Order::create_order();
 			$this->do_partial_refund( $order );
 		}
+		if ( true === $order_props['full_refund'] ) {
+			$order = WC_Helper_Order::create_order();
+			$this->do_full_refund( $order );
+		}

 		$order->set_billing_email( $order_props['billing_email'] );
+		if ( ! is_null( $order_props['created_via'] ) ) {
+			$order->set_created_via( $order_props['created_via'] );
+		}
 		$order->set_status( $order_props['status'] );
 		$order->save();

@@ -345,7 +638,9 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {

 		$request = new WP_REST_Request( 'POST', '/wc/v3/orders/' . $order->get_id() . '/actions/send_email' );
 		$request->add_header( 'User-Agent', 'some app' );
-		$request->set_param( 'template_id', $request_params['template_id'] );
+		if ( ! empty( $request_params['template_id'] ) ) {
+			$request->set_param( 'template_id', $request_params['template_id'] );
+		}
 		if ( ! empty( $request_params['email'] ) ) {
 			$request->set_param( 'email', $request_params['email'] );
 		}
@@ -368,6 +663,37 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
 		}
 	}

+	/**
+	 * @testdox The preferred template IDs filter hook overrides default template auto-selection.
+	 */
+	public function test_send_email_auto_select_respects_preferred_template_ids_filter() {
+		$order = WC_Helper_Order::create_order();
+		$order->set_billing_email( 'customer@example.org' );
+		$order->set_status( 'completed' );
+		$this->do_partial_refund( $order );
+		$order->save();
+
+		wp_set_current_user( $this->user['shop_manager'] );
+
+		// Override preferred template IDs to prefer invoice over the status-specific template.
+		$override_preferred_ids = function () {
+			return array( 'customer_invoice', 'customer_completed_order' );
+		};
+		add_filter( 'woocommerce_rest_order_actions_email_preferred_template_ids', $override_preferred_ids );
+
+		$request = new WP_REST_Request( 'POST', '/wc/v3/orders/' . $order->get_id() . '/actions/send_email' );
+		$request->add_header( 'User-Agent', 'some app' );
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$this->assertStringContainsString( 'Order details sent to', $data['message'] );
+
+		remove_filter( 'woocommerce_rest_order_actions_email_preferred_template_ids', $override_preferred_ids );
+	}
+
 	/**
 	 * Test sending order details email.
 	 */