Repository: woocommerce/woocommerce (PHP)

Issue:
Delete test orders in LYS hub
## Issue Description

Provide an option to delete test orders in LYS. For the first implementation, delete test orders made with the WooPayments payment gateway only. For details about it's test mode, see: https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/

We'll look to support a full range of payment gateways at a later date.

![Image](https://github.com/woocommerce/woocommerce/assets/9312929/73b96776-1906-4064-843a-89de5d0c0dfb)

[Figma](https://www.figma.com/design/0BWMerqRR1f468EwC7g5tg/LYS?node-id=835-28896&t=P21Ov9OujfOBd0bq-4)

## Acceptance Criteria

Using WooPayments test orders only,

* Implement the design above, using a toggle to delete test orders and in the label provide an accurate count of test orders in the system.
* The toggle should be enabled by default.
* Perform a bulk delete test orders when the user clicks "Launch your store button" and the option is toggled
* Detect errors and send a Tracks event, but continue launching the store regardless.


Discussion: **Dev note**

The [doc](https://woocommerce.com/document/managing-orders/testing-orders/) says:

> There is currently no visual difference or special status for test orders. As a result, other extensions, plugins, or services that integrate with WooCommerce, may be unable to differentiate between these test orders and your real orders.

However, test orders have `_wcpay_mode` meta data with `test` value stored in `wp_wc_orders_meta` table.



Fix (patch):
diff --git a/plugins/woocommerce-admin/client/launch-your-store/hub/sidebar/xstate.tsx b/plugins/woocommerce-admin/client/launch-your-store/hub/sidebar/xstate.tsx
index c937630a64c4f..c51f718894939 100644
--- a/plugins/woocommerce-admin/client/launch-your-store/hub/sidebar/xstate.tsx
+++ b/plugins/woocommerce-admin/client/launch-your-store/hub/sidebar/xstate.tsx
@@ -16,6 +16,7 @@ import { getQuery, navigateTo } from '@woocommerce/navigation';
 import { OPTIONS_STORE_NAME, TaskListType, TaskType } from '@woocommerce/data';
 import { dispatch } from '@wordpress/data';
 import { recordEvent } from '@woocommerce/tracks';
+import apiFetch from '@wordpress/api-fetch';
 
 /**
  * Internal dependencies
@@ -71,6 +72,31 @@ const launchStoreAction = async () => {
 	throw new Error( JSON.stringify( results ) );
 };
 
+const getTestOrderCount = async () => {
+	const result = ( await apiFetch( {
+		path: '/wc-admin/launch-your-store/woopayments/test-orders/count',
+		method: 'GET',
+	} ) ) as { count: number };
+
+	return result.count;
+};
+
+const deleteTestOrders = async ( {
+	input,
+}: {
+	input: {
+		removeTestOrders: boolean;
+	};
+} ) => {
+	if ( ! input.removeTestOrders ) {
+		return null;
+	}
+	return await apiFetch( {
+		path: '/wc-admin/launch-your-store/woopayments/test-orders',
+		method: 'DELETE',
+	} );
+};
+
 const recordStoreLaunchAttempt = ( {
 	context,
 }: {
@@ -172,11 +198,18 @@ export const sidebarMachine = setup( {
 			const { sidebar } = getQuery() as { sidebar?: string };
 			return !! sidebar && sidebar === sidebarLocation;
 		},
+		hasWooPayments: () => {
+			return window?.wcSettings?.admin?.plugins?.activePlugins.includes(
+				'woocommerce-payments'
+			);
+		},
 	},
 	actors: {
 		sidebarQueryParamListener,
 		getTasklist: fromPromise( getLysTasklist ),
+		getTestOrderCount: fromPromise( getTestOrderCount ),
 		updateLaunchStoreOptions: fromPromise( launchStoreAction ),
+		deleteTestOrders: fromPromise( deleteTestOrders ),
 		fetchCongratsData,
 	},
 } ).createMachine( {
@@ -228,6 +261,31 @@ export const sidebarMachine = setup( {
 							actions: assign( {
 								tasklist: ( { event } ) => event.output,
 							} ),
+							target: 'maybeCountTestOrders',
+						},
+					},
+				},
+				maybeCountTestOrders: {
+					always: [
+						{
+							guard: 'hasWooPayments',
+							target: 'countTestOrders',
+						},
+						{
+							target: 'launchYourStoreHub',
+						},
+					],
+				},
+				countTestOrders: {
+					invoke: {
+						src: 'getTestOrderCount',
+						onDone: {
+							actions: assign( {
+								testOrderCount: ( { event } ) => event.output,
+							} ),
+							target: 'launchYourStoreHub',
+						},
+						onError: {
 							target: 'launchYourStoreHub',
 						},
 					},
@@ -255,38 +313,52 @@ export const sidebarMachine = setup( {
 						assign( { launchStoreError: undefined } ), // clear the errors if any from previously
 						'recordStoreLaunchAttempt',
 					],
-					invoke: {
-						src: 'updateLaunchStoreOptions',
-						onDone: {
-							target: '#storeLaunchSuccessful',
-							actions: [
-								{
-									type: 'recordStoreLaunchResults',
-									params: { success: true },
-								},
-							],
-						},
-						onError: {
-							actions: [
-								assign( {
-									launchStoreError: ( { event } ) => {
-										return {
-											message: JSON.stringify(
-												event.error
-											), // for some reason event.error is an empty object, worth investigating if we decide to use the error message somewhere
-										};
+					invoke: [
+						{
+							src: 'updateLaunchStoreOptions',
+							onDone: {
+								target: '#storeLaunchSuccessful',
+								actions: [
+									{
+										type: 'recordStoreLaunchResults',
+										params: { success: true },
 									},
-								} ),
-								{
-									type: 'recordStoreLaunchResults',
-									params: {
-										success: false,
+								],
+							},
+							onError: {
+								actions: [
+									assign( {
+										launchStoreError: ( { event } ) => {
+											return {
+												message: JSON.stringify(
+													event.error
+												), // for some reason event.error is an empty object, worth investigating if we decide to use the error message somewhere
+											};
+										},
+									} ),
+									{
+										type: 'recordStoreLaunchResults',
+										params: {
+											success: false,
+										},
 									},
-								},
-							],
-							target: '#launchYourStoreHub',
+								],
+								target: '#launchYourStoreHub',
+							},
 						},
-					},
+						{
+							src: 'deleteTestOrders',
+							input: ( { event } ) => {
+								return {
+									removeTestOrders: (
+										event as {
+											removeTestOrders: boolean;
+										}
+									 ).removeTestOrders,
+								};
+							},
+						},
+					],
 				},
 			},
 		},
diff --git a/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss b/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss
index 1df735a7d03ef..c08912260d567 100644
--- a/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss
+++ b/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss
@@ -170,6 +170,10 @@
 								}
 							}
 
+							.edit-site-sidebar-navigation-screen-test-data__group-header {
+								margin-top: 40px;
+							}
+
 							.edit-site-sidebar-navigation-item {
 								border-radius: 4px;
 								padding: 8px 8px 8px 16px;
diff --git a/plugins/woocommerce-admin/client/typings/global.d.ts b/plugins/woocommerce-admin/client/typings/global.d.ts
index 2e0b865f6dae7..def5d1e58e5a9 100644
--- a/plugins/woocommerce-admin/client/typings/global.d.ts
+++ b/plugins/woocommerce-admin/client/typings/global.d.ts
@@ -13,6 +13,10 @@ declare global {
 				currentUserData: {
 					first_name: string;
 				};
+				plugins: {
+					activePlugins: string[];
+					installedPlugins: string[];
+				},
 				wcpayWelcomePageIncentive: {
 					id: string;
 					description: string;
diff --git a/plugins/woocommerce/src/Admin/API/LaunchYourStore.php b/plugins/woocommerce/src/Admin/API/LaunchYourStore.php
index bd0929df739e0..82eefbc86a5bf 100644
--- a/plugins/woocommerce/src/Admin/API/LaunchYourStore.php
+++ b/plugins/woocommerce/src/Admin/API/LaunchYourStore.php
@@ -77,6 +77,30 @@ public function register_routes() {
 				),
 			)
 		);
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/woopayments/test-orders/count',
+			array(
+				array(
+					'methods'             => 'GET',
+					'callback'            => array( $this, 'get_woopay_test_orders_count' ),
+					'permission_callback' => array( $this, 'must_be_shop_manager_or_admin' ),
+				),
+			)
+		);
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/woopayments/test-orders',
+			array(
+				array(
+					'methods'             => 'DELETE',
+					'callback'            => array( $this, 'delete_woopay_test_orders' ),
+					'permission_callback' => array( $this, 'must_be_shop_manager_or_admin' ),
+				),
+			)
+		);
 	}
 
 	/**
@@ -126,6 +150,55 @@ public function initialize_coming_soon() {
 		return true;
 	}
 
+	/**
+	 * Count the test orders created during Woo Payments test mode.
+	 *
+	 * @return \WP_REST_Response
+	 */
+	public function get_woopay_test_orders_count() {
+		$return = function ( $count ) {
+			return new \WP_REST_Response( array( 'count' => $count ) );
+		};
+
+		$orders = wc_get_orders(
+			array(
+				// phpcs:ignore
+				'meta_key'   => '_wcpay_mode',
+				// phpcs:ignore
+				'meta_value' => 'test',
+				'return'     => 'ids',
+			)
+		);
+
+		return $return( count( $orders ) );
+	}
+
+	/**
+	 * Delete WooPayments test orders.
+	 *
+	 * @return \WP_REST_Response
+	 */
+	public function delete_woopay_test_orders() {
+		$return = function ( $status = 204 ) {
+			return new \WP_REST_Response( null, $status );
+		};
+
+		$orders = wc_get_orders(
+			array(
+				// phpcs:ignore
+				'meta_key'   => '_wcpay_mode',
+				// phpcs:ignore
+				'meta_value' => 'test',
+			)
+		);
+
+		foreach ( $orders as $order ) {
+			$order->delete();
+		}
+
+		return $return();
+	}
+
 	/**
 	 * Update woocommerce_admin_launch_your_store_survey_completed to yes or no
 	 *