diff --git a/plugins/woocommerce/changelog/52050-51797-new-receipts-endpoint b/plugins/woocommerce/changelog/52050-51797-new-receipts-endpoint new file mode 100644 index 00000000000..19c19c61f40 --- /dev/null +++ b/plugins/woocommerce/changelog/52050-51797-new-receipts-endpoint @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add the new `POST /orders/{id}/actions` endpoint to allow re-sending the order details to customers. \ No newline at end of file diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php index 8dcb5247805..567403e4e46 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php @@ -103,7 +103,7 @@ class WC_Meta_Box_Order_Actions { $trash_order_url = add_query_arg( array( 'action' => 'trash', - 'id' => array( $order_id ), + 'id' => array( $order_id ), '_wp_http_referer' => $order_list_url, ), $order_list_url @@ -131,6 +131,11 @@ class WC_Meta_Box_Order_Actions { $action = wc_clean( wp_unslash( $_POST['wc_order_action'] ) ); // @codingStandardsIgnoreLine if ( 'send_order_details' === $action ) { + /** + * Fires before an order email is resent. + * + * @since 1.0.0 + */ do_action( 'woocommerce_before_resend_order_emails', $order, 'customer_invoice' ); // Send the customer invoice email. @@ -141,6 +146,11 @@ class WC_Meta_Box_Order_Actions { // Note the event. $order->add_order_note( __( 'Order details manually sent to customer.', 'woocommerce' ), false, true ); + /** + * Fires after an order email has been resent. + * + * @since 1.0.0 + */ do_action( 'woocommerce_after_resend_order_email', $order, 'customer_invoice' ); // Change the post saved message. diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersControllersServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersControllersServiceProvider.php index 433c479e688..750e6ab863f 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersControllersServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersControllersServiceProvider.php @@ -7,12 +7,13 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; use Automattic\WooCommerce\Internal\Orders\CouponsController; +use Automattic\WooCommerce\Internal\Orders\OrderActionsRestController; use Automattic\WooCommerce\Internal\Orders\TaxesController; /** * Service provider for the orders controller classes in the Automattic\WooCommerce\Internal\Orders namespace. */ -class OrdersControllersServiceProvider extends AbstractServiceProvider { +class OrdersControllersServiceProvider extends AbstractInterfaceServiceProvider { /** * The classes/interfaces that are serviced by this service provider. @@ -22,6 +23,7 @@ class OrdersControllersServiceProvider extends AbstractServiceProvider { protected $provides = array( CouponsController::class, TaxesController::class, + OrderActionsRestController::class, ); /** @@ -30,5 +32,6 @@ class OrdersControllersServiceProvider extends AbstractServiceProvider { public function register() { $this->share( CouponsController::class ); $this->share( TaxesController::class ); + $this->share_with_implements_tags( OrderActionsRestController::class ); } } diff --git a/plugins/woocommerce/src/Internal/Orders/OrderActionsRestController.php b/plugins/woocommerce/src/Internal/Orders/OrderActionsRestController.php new file mode 100644 index 00000000000..f6fca50883a --- /dev/null +++ b/plugins/woocommerce/src/Internal/Orders/OrderActionsRestController.php @@ -0,0 +1,135 @@ +route_namespace, + '/orders/(?P[\d]+)/actions/send_order_details', + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => fn( $request ) => $this->run( $request, 'send_order_details' ), + 'permission_callback' => fn( $request ) => $this->check_permissions( $request ), + 'args' => $this->get_args_for_order_actions(), + 'schema' => $this->get_schema_for_order_actions(), + ), + ) + ); + } + + /** + * Permission check for REST API endpoint. + * + * @param WP_REST_Request $request The request for which the permission is checked. + * @return bool|WP_Error True if the current user has the capability, otherwise a WP_Error object. + */ + private function check_permissions( WP_REST_Request $request ) { + $order_id = $request->get_param( 'id' ); + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_not_found', __( 'Order not found', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $this->check_permission( $request, 'read_shop_order', $order_id ); + } + + /** + * Get the accepted arguments for the POST request. + * + * @return array[] + */ + private function get_args_for_order_actions(): array { + return array( + 'id' => array( + 'description' => __( 'Unique identifier of the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ); + } + + /** + * Get the schema for both the GET and the POST requests. + * + * @return array[] + */ + private function get_schema_for_order_actions(): array { + $schema['properties'] = array( + 'message' => array( + 'description' => __( 'A message indicating that the action completed successfully.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ); + return $schema; + } + + /** + * Handle the POST /orders/{id}/actions/send_order_details. + * + * @param WP_REST_Request $request The received request. + * @return array|WP_Error Request response or an error. + */ + public function send_order_details( WP_REST_Request $request ) { + $order_id = $request->get_param( 'id' ); + $order = wc_get_order( $order_id ); + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment + /** This action is documented in includes/admin/meta-boxes/class-wc-meta-box-order-actions.php */ + do_action( 'woocommerce_before_resend_order_emails', $order, 'customer_invoice' ); + + WC()->payment_gateways(); + WC()->shipping(); + WC()->mailer()->customer_invoice( $order ); + + $user_agent = esc_html( $request->get_header( 'User-Agent' ) ); + $note = sprintf( + // translators: %1$s is the customer email, %2$s is the user agent that requested the action. + esc_html__( 'Order details sent to %1$s, via %2$s.', 'woocommerce' ), + esc_html( $order->get_billing_email() ), + $user_agent ? $user_agent : 'REST API' + ); + $order->add_order_note( $note, false, true ); + + // phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment + /** This action is documented in includes/admin/meta-boxes/class-wc-meta-box-order-actions.php */ + do_action( 'woocommerce_after_resend_order_email', $order, 'customer_invoice' ); + + return array( + 'message' => __( 'Order details email sent to customer.', 'woocommerce' ), + ); + } +} diff --git a/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php new file mode 100644 index 00000000000..bb0f5ae536b --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php @@ -0,0 +1,96 @@ +controller = new OrderActionsRestController(); + $this->controller->register_routes(); + + $this->user = $this->factory->user->create( array( 'role' => 'shop_manager' ) ); + } + + /** + * Test sending order details email. + */ + public function test_send_order_details() { + $order = wc_create_order(); + $order->set_billing_email( 'customer@email.com' ); + $order->save(); + + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/orders/' . $order->get_id() . '/actions/send_order_details' ); + $request->add_header( 'User-Agent', 'some app' ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'message', $data ); + $this->assertEquals( 'Order details email sent to customer.', $data['message'] ); + + $notes = wc_get_order_notes( array( 'order_id' => $order->get_id() ) ); + $this->assertCount( 1, $notes ); + $this->assertEquals( 'Order details sent to customer@email.com, via some app.', $notes[0]->content ); + } + + /** + * Test sending order details email for a non-existent order. + */ + public function test_send_order_details_with_non_existent_order() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/orders/999/actions/send_order_details' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 'woocommerce_rest_not_found', $data['code'] ); + $this->assertEquals( 'Order not found', $data['message'] ); + } + + /** + * Test sending order details email without proper permissions. + */ + public function test_send_order_details_without_permission() { + $order = wc_create_order(); + + // Use a customer user who shouldn't have permission. + $customer = $this->factory->user->create( array( 'role' => 'customer' ) ); + + wp_set_current_user( $customer ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/orders/' . $order->get_id() . '/actions/send_order_details' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 403, $response->get_status() ); + } +}