Repository: woocommerce/woocommerce
Pull request #56089: Added caching to Tax Onboarding Task.
Author: login: prettyboymp
Labels: name: plugin: woocommerce
description: Issues related to the WooCommerce Core plugin.
name: needs: analysis
description: Indicates if the PR requires a PR testing scrub session.
Created: 2025-03-04T18:38:00Z | Merged: 2025-03-18T17:43:31Z

### Submission Review Guidelines:

-   I have followed the [WooCommerce Contributing Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) and the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/).
-   I have checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/woocommerce/pulls) for the same update/change.
-   I have reviewed my code for [security best practices](https://developer.wordpress.org/apis/security/).
-   Following the above guidelines will result in quick merges and clear and detailed feedback when appropriate.

<!-- You can erase any parts of this template not applicable to your Pull Request. -->

### Changes proposed in this Pull Request:

This PR does the following:

1.  Adds caching around the check for existing tax rates to avoid the need to repeatedly query the tax rates.
2. Marks the Tax task as actioned once a tax rate exists as an indefinite completion marker.  This allows the task to resurface if the 3rd party implementation is uninstalled but no tax rates were ever created.

Closes #54908 .

### How to test the changes in this Pull Request:

<!-- Include detailed instructions on how these changes can be tested. Review and follow the guide for how to write high-quality testing instructions. -->

Using the [WooCommerce Testing Instructions Guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions), include your detailed testing instructions:

1.  Setup a site from scratch, or clear any tax rates and the following option values: `woocommerce_task_list_tracked_completed_tasks`, `woocommerce_no_sales_tax`, `woocommerce_task_list_tracked_completed_actions`.
2. Use a query monitoring plugin to watch for queries.
3. Visit the WooCommerce Admin to view the current Onboarding Task List.
4. The tax task should show as incomplete.
![Screenshot 2025-03-04 at 1 35 03 PM](https://github.com/user-attachments/assets/a7866869-7fbe-4d4f-8d80-862ac3cbd73a)
5. Verify that the query `SELECT 1 {$wpdb->prefix}woocommerce_tax_rates limit 1` only run during the initial load (if not already cached from the Dashboard metabox).
6. Go to setup taxes manually and insert a new tax rate.
7. Revisit the WooCommerce Admin and verify that the task now shows as complete and that the query against the tax rates is no longer being run.  This should be the case even if cache is cleared.
<!-- End testing instructions -->


Linked issue #54908: [Performance]: Improve performance of OnboardingTask\Tax::is_complete() check
### Prerequisites

- [x] I have carried out troubleshooting steps and I believe I have identified a performance issue.
- [x] I have searched for similar bugs in both open and closed issues and cannot find a duplicate.

### Performance Details

The onboarding task for Tax setup attempts to check if any taxes have been added to determine if the task is complete.  However, it does so by loading and counting all the taxes that exist in the database.

https://github.com/woocommerce/woocommerce/blob/5acc35da54786fdf900bf81e7297aede64ee658f/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php#L119-L123

This is run on every admin page load.

### Affected Sites

This affects sites where there are a lot of tax locations, e.g. where a 3rd party API service isn't being used.  There are no filters on the query, so the speed of the query is solely based on the amount of data in the tax_rates table.

On a local site with ~30k tax locations loaded (3/4's of current US zip codes), it takes around 35ms to run the query (not including processing/object hydration).

### Suspected causes

_No response_

### Other context

## Suggested Improvements

* Modify the check for existing taxes so that the query only needs to check for the existence of a tax rate rather than loading the entire dataset.
* Cache the result of the check for existing tax rates. Make sure cache is cleared on relevant actions.
Linked issue comments: user: login: prettyboymp
created_at: 2025-02-06T14:04:40Z
body: Marking as high priority just for the fact that it runs on every page load.
author_association: CONTRIBUTOR
user: login: prettyboymp
created_at: 2025-02-12T16:00:29Z
body: See https://github.com/woocommerce/woocommerce/issues/54443#issuecomment-2654147447 - we have a way to track completed tasks already that would be more efficient to check before additional queries or cache checks.
author_association: CONTRIBUTOR

Diff:
diff --git a/plugins/woocommerce/changelog/fix-54908-tax-onboarding-task-performance b/plugins/woocommerce/changelog/fix-54908-tax-onboarding-task-performance
new file mode 100644
index 0000000000000..f747a29736d6d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-54908-tax-onboarding-task-performance
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Added caching to Tax Onboarding Task.
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
index 1268d618dd812..02775ee7add98 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
@@ -391,7 +391,9 @@ public function has_previously_completed() {
 	}
 
 	/**
-	 * Track task completion if task is viewable.
+	 * Track task completion if task is viewable and is complete.
+	 *
+	 * @return void
 	 */
 	public function possibly_track_completion() {
 		if ( $this->has_previously_completed() ) {
@@ -499,7 +501,10 @@ public function get_record_view_event(): bool {
 	 * @return array
 	 */
 	public function get_json() {
-		$this->possibly_track_completion();
+		$is_complete = $this->is_complete();
+		if ( $is_complete ) {
+			$this->possibly_track_completion();
+		}
 
 		return array(
 			'id'              => $this->get_id(),
@@ -511,7 +516,7 @@ public function get_json() {
 			'additionalInfo'  => $this->get_additional_info(),
 			'actionLabel'     => $this->get_action_label(),
 			'actionUrl'       => $this->get_action_url(),
-			'isComplete'      => $this->is_complete(),
+			'isComplete'      => $is_complete,
 			'time'            => $this->get_time(),
 			'level'           => 3,
 			'isActioned'      => $this->is_actioned(),
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
index b0a65b969e6ed..0171912838ef9 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
@@ -28,6 +28,7 @@ class Tax extends Task {
 	public function __construct( $task_list ) {
 		parent::__construct( $task_list );
 		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_return_notice_script' ) );
+		add_action( 'woocommerce_tax_rate_added', array( $this, 'track_actioned_on_tax_rate_added' ) );
 	}
 
 	/**
@@ -117,14 +118,46 @@ public function is_complete() {
 			$third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false );
 
 			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
-				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
-				get_option( 'woocommerce_no_sales_tax' ) !== false ||
-				$third_party_complete;
+				$third_party_complete ||
+				false !== get_option( 'woocommerce_no_sales_tax' ) ||
+				$this->is_actioned();
+
+			if ( ! $this->is_complete_result && $this->has_existing_tax_rates() ) {
+				$this->mark_actioned();
+				$this->is_complete_result = true;
+			}
 		}
 
 		return $this->is_complete_result;
 	}
 
+	/**
+	 * Determines if a tax rate exists in the database.  Result is indefinitely cached.
+	 *
+	 * @return bool
+	 */
+	private function has_existing_tax_rates() {
+		global $wpdb;
+
+		$has_existing_tax_rates = wp_cache_get( 'woocommerce_onboarding_task_tax_rates_exist' );
+		if ( false === $has_existing_tax_rates ) {
+			$rate_exists            = (bool) $wpdb->get_var( "SELECT 1 FROM {$wpdb->prefix}woocommerce_tax_rates limit 1" );
+			$has_existing_tax_rates = $rate_exists ? 'yes' : 'no';
+			wp_cache_set( 'woocommerce_onboarding_task_tax_rates_exist', $has_existing_tax_rates );
+		}
+
+		return 'yes' === $has_existing_tax_rates;
+	}
+
+	/**
+	 * Marks the task as actioned any time a tax rate has been added. Called from the `woocommerce_tax_rate_added` hook.
+	 *
+	 * @return void
+	 */
+	public function track_actioned_on_tax_rate_added() {
+		$this->mark_actioned();
+	}
+
 	/**
 	 * Additional data.
 	 *


Reviews:
user: login: joshuatf
body: Nice optimization!  Left one comment on how we might be able to implement a cached `is_complete` across all tasks.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:54:16Z
user: login: prettyboymp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:35:39Z
user: login: joshuatf
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:08:51Z
user: login: joshuatf
body: Testing well with a couple very minor modifications.

* ✅ Tax query is hit on first load only and not subsequent loads
* ✅ Caching prevents query from being re-run when persistent object cache is enabled
* ✅ Once task is actioned, the check for taxes no longer gets hit
* ⚠️ SQL query always returns `1`; fixed by addition of `FROM`
* ⚠️ Initially complete task shows incomplete on first load

Pre-approving this PR as tested with the 2 update suggestions left in the comments.  Feel free to ping me if you need another review, but I've tested with those changes and things are working well otherwise.

<img width="722" alt="Screenshot 2025-03-17 at 3 48 49 PM" src="https://github.com/user-attachments/assets/823883ca-3a41-4ed2-b0af-adbd32ba37f2" />

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T19:49:20Z
user: login: kalessil
body: Like the optimization added in this PR! Leaving a couple of comments to consider as well.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:56:42Z

Review comments:
diff_hunk: @@ -391,15 +391,19 @@ public function has_previously_completed() {
 	}
 
 	/**
-	 * Track task completion if task is viewable.
+	 * Track task completion if task is viewable and is complete.
+	 *
+	 * @param bool $verify_is_complete Whether to test if $this->complete() prior to tracking completion. Default true.
+	 *
+	 * @return void
 	 */
-	public function possibly_track_completion() {
+	public function possibly_track_completion( $verify_is_complete = true ) {
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
user: login: joshuatf
body: Instead of adding an additional argument here, could we refactor `is_complete()` to cache its value in a `is_complete` property inside the instance so we can access it without needing to recalculate?
created_at: 2025-03-10T14:46:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,14 +118,45 @@ public function is_complete() {
 			$third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false );
 
 			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
-				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
-				get_option( 'woocommerce_no_sales_tax' ) !== false ||
-				$third_party_complete;
+                   get_option( 'woocommerce_no_sales_tax' ) !== false ||
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
user: login: joshuatf
body: Just double checking, but seems like we're putting the less expensive checks first so we might not need to run the more expensive checks, right?

If so, 👍 
created_at: 2025-03-10T14:49:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,14 +118,45 @@ public function is_complete() {
 			$third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false );
 
 			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
-				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
-				get_option( 'woocommerce_no_sales_tax' ) !== false ||
-				$third_party_complete;
+                   get_option( 'woocommerce_no_sales_tax' ) !== false ||
+                   $third_party_complete ||
+                   $this->is_actioned();
+
+			if ( ! $this->is_complete_result && $this->has_existing_tax_rates() ) {
+				$this->mark_actioned();
+			}
 		}
 
 		return $this->is_complete_result;
 	}
 
+	/**
+	 * Determines if a tax rate exists in the database.  Result is indefinitely cached.
+	 *
+	 * @return bool
+	 */
+	private function has_existing_tax_rates() {
+		global $wpdb;
+
+		$has_existing_tax_rates = wp_cache_get( 'onboarding_task_tax_rates_exist' );
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
user: login: joshuatf
body: Should we prefix these with `woocommerce_`?
created_at: 2025-03-10T14:50:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,6 +28,7 @@ class Tax extends Task {
 	public function __construct( $task_list ) {
 		parent::__construct( $task_list );
 		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_return_notice_script' ) );
+		add_action( 'woocommerce_tax_rate_added', array( $this, 'track_actioned_on_tax_rate_added' ) );
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
user: login: joshuatf
body: Nice addition!
created_at: 2025-03-10T14:50:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -391,15 +391,19 @@ public function has_previously_completed() {
 	}
 
 	/**
-	 * Track task completion if task is viewable.
+	 * Track task completion if task is viewable and is complete.
+	 *
+	 * @param bool $verify_is_complete Whether to test if $this->complete() prior to tracking completion. Default true.
+	 *
+	 * @return void
 	 */
-	public function possibly_track_completion() {
+	public function possibly_track_completion( $verify_is_complete = true ) {
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
user: login: prettyboymp
body: @joshuatf, I initially attempted to avoid adding the additional parameter, but since each `Task` implementation essentially overrides `::is_complete()`, there isn't a great way to do that, especially since some Tasks are implemented in other extensions, e.g. WooCommerce Payments.

This isn't an improvement specifically needed for the Tax Task, but for the way `Task::get_json()` is used because the current implementation has `::is_complete()` get called twice, which in some Tasks, is expensive.  So this was an attempt to optimize external task implementations a little as well.  I'm good leaving it out if that is preferred.


created_at: 2025-03-11T17:35:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -391,15 +391,19 @@ public function has_previously_completed() {
 	}
 
 	/**
-	 * Track task completion if task is viewable.
+	 * Track task completion if task is viewable and is complete.
+	 *
+	 * @param bool $verify_is_complete Whether to test if $this->complete() prior to tracking completion. Default true.
+	 *
+	 * @return void
 	 */
-	public function possibly_track_completion() {
+	public function possibly_track_completion( $verify_is_complete = true ) {
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
user: login: joshuatf
body: Ya, I see what you mean and get how this gives us some immediate performance gains in the short-term.  I think my preference would be to refactor `is_complete()` to cover these cases.

```php
abstract Task {
    /**
     * Cache completion for each page load.
     *
     * @var bool|null
     */
    private $is_complete = null;

    public function is_complete() {
        if ( null !== $this->is_complete ) {
            return $this->is_complete;
        }

        if ( $this->is_actioned() ) {
            return true;
        }

        if ( $this->has_previously_completed() ) {
            return true;
        }

        $this->is_complete = $this->calculate_completion(); // Naming TBD.
        return $this->is_complete;
    }
}

class ConcreteTask {
    /**
     * Calculate if a task is complete, bypassing any caching.
     *
     * @return bool
     */
    private function calculate_completion() {
        // Run expensive queries to check for completion.
        return $result;
    }
}
```

This accomplishes two things:

1. We get built-in caching so that other calls to `is_complete()` don't re-query during the same page load.  Note that some tasks have already [taken it upon themselves to do this](https://github.com/woocommerce/woocommerce/blob/97647674de2273eab3abf12f30ded84fe51c1bdd/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php/#L14-L19).
2. We completely bypass the need to query at all if consumers opt-in to the new usage.  I opened a thread for discussion in the @woocommerce/ghidorah Slack channel to check current expectations around tasks and keeping them marked complete.
created_at: 2025-03-13T17:05:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -391,15 +391,19 @@ public function has_previously_completed() {
 	}
 
 	/**
-	 * Track task completion if task is viewable.
+	 * Track task completion if task is viewable and is complete.
+	 *
+	 * @param bool $verify_is_complete Whether to test if $this->complete() prior to tracking completion. Default true.
+	 *
+	 * @return void
 	 */
-	public function possibly_track_completion() {
+	public function possibly_track_completion( $verify_is_complete = true ) {
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
user: login: joshuatf
body: Obviously this means a couple external tasks, like those from WooCommerce Payments, might not get the immediate benefit, but the change is trivial for them to make and take advantage of the performance gains.

I'm also okay with this change in a follow-up if you prefer, though I'd like to avoid the additional logic in `possibly_track_completion` as I think it adds complexity.  If we go this route, maybe we could just apply the performance changes to the Tax task.
created_at: 2025-03-13T17:08:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,14 +118,45 @@ public function is_complete() {
 			$third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false );
 
 			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
-				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
-				get_option( 'woocommerce_no_sales_tax' ) !== false ||
-				$third_party_complete;
+				false !== get_option( 'woocommerce_no_sales_tax' ) ||
+				$third_party_complete ||
+				$this->is_actioned();
+
+			if ( ! $this->is_complete_result && $this->has_existing_tax_rates() ) {
+				$this->mark_actioned();
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
user: login: joshuatf
body: We should also add `$this->is_complete_result = true;` in here otherwise this function still returns `false` the first time even though its actioned.  This results in the task being marked incomplete the first time and complete on subsequent reloads.

**First load**

<img width="720" alt="Screenshot 2025-03-17 at 3 32 04 PM" src="https://github.com/user-attachments/assets/f5592c17-b70a-40b0-99e0-70a4b491be3f" />

**Reload**
<img width="716" alt="Screenshot 2025-03-17 at 3 33 06 PM" src="https://github.com/user-attachments/assets/ae3e28da-8e35-441b-a6b6-cc51611d8747" />


created_at: 2025-03-17T19:43:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,14 +118,45 @@ public function is_complete() {
 			$third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false );
 
 			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
-				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
-				get_option( 'woocommerce_no_sales_tax' ) !== false ||
-				$third_party_complete;
+				false !== get_option( 'woocommerce_no_sales_tax' ) ||
+				$third_party_complete ||
+				$this->is_actioned();
+
+			if ( ! $this->is_complete_result && $this->has_existing_tax_rates() ) {
+				$this->mark_actioned();
+			}
 		}
 
 		return $this->is_complete_result;
 	}
 
+	/**
+	 * Determines if a tax rate exists in the database.  Result is indefinitely cached.
+	 *
+	 * @return bool
+	 */
+	private function has_existing_tax_rates() {
+		global $wpdb;
+
+		$has_existing_tax_rates = wp_cache_get( 'woocommerce_onboarding_task_tax_rates_exist' );
+		if ( false === $has_existing_tax_rates ) {
+			$rate_exists            = (bool) $wpdb->get_var( "SELECT 1 {$wpdb->prefix}woocommerce_tax_rates limit 1" );
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
user: login: joshuatf
body: This query will always return `1`; we'll need a `FROM` in here:

```suggestion
			$rate_exists            = (bool) $wpdb->get_var( "SELECT 1 FROM {$wpdb->prefix}woocommerce_tax_rates limit 1" );
```
created_at: 2025-03-17T19:43:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,14 +118,45 @@ public function is_complete() {
 			$third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false );
 
 			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
-				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
-				get_option( 'woocommerce_no_sales_tax' ) !== false ||
-				$third_party_complete;
+				false !== get_option( 'woocommerce_no_sales_tax' ) ||
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
user: login: kalessil
body: Guess we can micro-optimize this a bit:

```php
			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
				$third_party_complete ||
				false !== get_option( 'woocommerce_no_sales_tax' ) ||
				$this->is_actioned();
```
created_at: 2025-03-18T07:19:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,14 +118,45 @@ public function is_complete() {
 			$third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false );
 
 			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
-				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
-				get_option( 'woocommerce_no_sales_tax' ) !== false ||
-				$third_party_complete;
+				false !== get_option( 'woocommerce_no_sales_tax' ) ||
+				$third_party_complete ||
+				$this->is_actioned();
+
+			if ( ! $this->is_complete_result && $this->has_existing_tax_rates() ) {
+				$this->mark_actioned();
+			}
 		}
 
 		return $this->is_complete_result;
 	}
 
+	/**
+	 * Determines if a tax rate exists in the database.  Result is indefinitely cached.
+	 *
+	 * @return bool
+	 */
+	private function has_existing_tax_rates() {
+		global $wpdb;
+
+		$has_existing_tax_rates = wp_cache_get( 'woocommerce_onboarding_task_tax_rates_exist' );
+		if ( false === $has_existing_tax_rates ) {
+			$rate_exists            = (bool) $wpdb->get_var( "SELECT 1 {$wpdb->prefix}woocommerce_tax_rates limit 1" );
path: plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
user: login: kalessil
body: Shouldn't [this approach](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php#L709) perform slightly faster?

`0 !== ( (int) $wpdb->get_var( "SELECT EXISTS (SELECT 1 FROM {$wpdb->prefix}woocommerce_tax_rates)" ) )` in our case and IIRC it's performance is not depending on having/not having `limit 1` in the inner query.
created_at: 2025-03-18T07:37:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-06T20:59:38Z
body: ## Testing Guidelines
Hi @joshuatf ,

Apart from reviewing the code changes, please make sure to **review the testing instructions** ([Guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions)) and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.

**Reminder:** PR reviewers are required to document testing performed. This includes:
- 🖼️ **Screenshots or screen recordings**.
- 📝 **List of functionality tested / steps followed**.
- 🌐 **Site details** (environment attributes such as hosting type, plugins, theme, store size, store age, and relevant settings).
- 🔍 **Any analysis performed**, such as assessing potential impacts on environment attributes and other plugins, conducting performance profiling, or using LLM/AI-based analysis.

⚠️ Within the testing details you provide, please ensure that no sensitive information (such as API keys, passwords, user data, etc.) is included in this public issue.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T21:06:20Z
body: 
## Test using WordPress Playground
The changes in this pull request can be previewed and tested using a [WordPress Playground](https://developer.wordpress.org/playground/) instance.
[WordPress Playground](https://developer.wordpress.org/playground/) is an experimental project that creates a full WordPress instance entirely within the browser.

[Test this pull request with WordPress Playground](https://playground.wordpress.net/#{"landingPage":"/wp-admin/admin.php?page=wc-admin","preferredVersions":{"php":"8.0","wp":"latest"},"phpExtensionBundles":["kitchen-sink"],"features":{"networking":true},"steps":[{"step":"installPlugin","pluginZipFile":{"resource":"url","url":"https://playground.wordpress.net/plugin-proxy.php?org=woocommerce&repo=woocommerce&workflow=Build%20Live%20Branch&artifact=plugins-13927172001&pr=56089"},"options":{"activate":true}},{"step":"installPlugin","pluginZipFile":{"resource":"url","url":"https://github-proxy.com/https://github.com/woocommerce/woocommerce/releases/download/wc-beta-tester-2.3.1/woocommerce-beta-tester.zip"},"options":{"activate":true}},{"step":"login","username":"admin","password":"password"}],"plugins":[]}).

Note that this URL is valid for 30 days from when this comment was last updated. You can update it by closing/reopening the PR or pushing a new commit.

author_association: CONTRIBUTOR