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 "Completed order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Completed order" 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 "Refunded order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Refunded order" 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 "Completed order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Completed order" 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 "Failed order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Failed order" 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 "Order on-hold" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Order on-hold" 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 "Processing order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Processing order" 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 "Failed order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Failed order" 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 "Order on-hold" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "Order on-hold" 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 "POS completed order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "POS completed order" 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 "POS refunded order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "POS refunded order" 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 "POS refunded order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "POS refunded order" 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 "POS completed order" sent to customer@example.org.',
+ 'notes' => array(
+ 'Email template "POS completed order" 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 "Completed order" sent to another@example.org.',
+ 'notes' => array(
+ 'Billing email updated to another@example.org.',
+ 'Email template "Completed order" 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.
*/