Commit 0d1f44b3a0 for woocommerce

commit 0d1f44b3a0c5539342f7502f34ac59a7e8505dae
Author: Hannah Tinkler <hannah.tinkler@gmail.com>
Date:   Thu Feb 5 12:32:44 2026 +0000

    Updates how exceptions are handled to use `WC_Data_Exception` (#63045)

    * Refactor PushNotifications library to use WC_Data_Exception.

    - Add PushTokenInvalidDataException extending WC_Data_Exception
    - Update PushTokenNotFoundException to extend WC_Data_Exception
    - Replace InvalidArgumentException with PushTokenInvalidDataException
    - Simplify convert_exception_to_wp_error to use getErrorCode/getCode

diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
index 6f509d1ec7..0cfafa88cd 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
@@ -9,10 +9,13 @@ defined( 'ABSPATH' ) || exit;
 use Automattic\WooCommerce\Internal\PushNotifications\DataStores\PushTokensDataStore;
 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
 use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenNotFoundException;
+use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenInvalidDataException;
 use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications;
 use Automattic\WooCommerce\Internal\RestApiControllerBase;
-use InvalidArgumentException;
+use Automattic\WooCommerce\Proxies\LegacyProxy;
 use Exception;
+use WC_Data_Exception;
+use WC_Logger;
 use WP_REST_Server;
 use WP_REST_Request;
 use WP_REST_Response;
@@ -160,16 +163,19 @@ class PushTokenRestController extends RestApiControllerBase {
 	}

 	/**
-	 * Validates the token.
+	 * Validates the token. This is done here as some of the validation rules
+	 * depend on other parameters. Calls `validate_argument` after to check the
+	 * PushToken validation rules which don't depend on other parameters.
 	 *
 	 * @since 10.6.0
 	 *
 	 * @param string          $token The token string.
 	 * @param WP_REST_Request $request The request object.
 	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+	 * @param string          $param The name of the parameter being validated.
 	 * @return bool|WP_Error
 	 */
-	public function validate_token( string $token, WP_REST_Request $request ) {
+	public function validate_token( string $token, WP_REST_Request $request, string $param ) {
 		if (
 			$request->get_param( 'platform' ) === PushToken::PLATFORM_APPLE
 			&& ! preg_match( '/^[A-Fa-f0-9]{64}$/', $token )
@@ -225,21 +231,24 @@ class PushTokenRestController extends RestApiControllerBase {
 			}
 		}

-		return true;
+		return $this->validate_argument( $token, $request, $param );
 	}

 	/**
 	 * Validates the device UUID, which is required unless the token is for
-	 * a browser.
+	 * a browser. This is done here as some of the validation rules depend on
+	 * other parameters. Calls `validate_argument` after to check the PushToken
+	 * validation rules which don't depend on other parameters.
 	 *
 	 * @since 10.6.0
 	 *
-	 * @param string          $device_uuid The device UUID string.
+	 * @param null|string     $device_uuid The device UUID string.
 	 * @param WP_REST_Request $request The request object.
 	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+	 * @param string          $param The name of the parameter being validated.
 	 * @return bool|WP_Error
 	 */
-	public function validate_device_uuid( ?string $device_uuid, WP_REST_Request $request ) {
+	public function validate_device_uuid( ?string $device_uuid, WP_REST_Request $request, string $param ) {
 		if (
 			! $device_uuid
 			&& $request->get_param( 'platform' ) !== PushToken::PLATFORM_BROWSER
@@ -271,6 +280,41 @@ class PushTokenRestController extends RestApiControllerBase {
 			);
 		}

+		return $this->validate_argument( $device_uuid, $request, $param );
+	}
+
+	/**
+	 * Validates the other arguments from the request, if validation exists in
+	 * the PushToken entity.
+	 *
+	 * @since 10.6.0
+	 *
+	 * @param mixed           $value The value being validated.
+	 * @param WP_REST_Request $request The request object.
+	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+	 * @param string          $param The name of the parameter being validated.
+	 * @return bool|WP_Error
+	 */
+	public function validate_argument( $value, WP_REST_Request $request, string $param ) {
+		$method = "set_$param";
+
+		if ( method_exists( PushToken::class, $method ) ) {
+			try {
+				$value     = rest_sanitize_request_arg( $value, $request, $param );
+				$validator = new PushToken();
+				$validator->{ "set_$param" }( $value );
+			} catch ( PushTokenInvalidDataException $e ) {
+				return new WP_Error(
+					$e->getErrorCode(),
+					$e->getMessage(),
+					array(
+						'status' => $e->getCode(),
+						'param'  => $param,
+					)
+				);
+			}
+		}
+
 		return true;
 	}

@@ -348,23 +392,32 @@ class PushTokenRestController extends RestApiControllerBase {
 	 * @return WP_Error
 	 */
 	private function convert_exception_to_wp_error( Exception $e ): WP_Error {
-		$exception_class = get_class( $e );
+		/**
+		 * If the exception is `WC_Data_Exception`, and doesn't represent an
+		 * internal server error (which may contain internal details that should
+		 * be obscured) then format it as a `WP_Error`.
+		 */
+		if (
+			$e instanceof WC_Data_Exception
+			&& $e->getCode() !== WP_Http::INTERNAL_SERVER_ERROR
+		) {
+			return new WP_Error(
+				$e->getErrorCode(),
+				$e->getMessage(),
+				$e->getErrorData()
+			);
+		}

-		$slugs = array(
-			PushTokenNotFoundException::class => 'woocommerce_rest_invalid_push_token',
-			InvalidArgumentException::class   => 'woocommerce_rest_invalid_argument',
-		);
+		wc_get_container()
+			->get( LegacyProxy::class )
+			->call_function( 'wc_get_logger' )
+			->error( (string) $e->getMessage(), array( 'source' => PushNotifications::FEATURE_NAME ) );

-		$statuses = array(
-			PushTokenNotFoundException::class => WP_Http::NOT_FOUND,
-			InvalidArgumentException::class   => WP_Http::BAD_REQUEST,
+		return new WP_Error(
+			'woocommerce_internal_error',
+			'Internal server error',
+			array( 'status' => WP_Http::INTERNAL_SERVER_ERROR )
 		);
-
-		$slug    = $slugs[ $exception_class ] ?? 'woocommerce_rest_internal_error';
-		$status  = $statuses[ $exception_class ] ?? WP_Http::INTERNAL_SERVER_ERROR;
-		$message = ! isset( $slugs[ $exception_class ] ) ? 'Internal server error' : $e->getMessage();
-
-		return new WP_Error( $slug, $message, array( 'status' => $status ) );
 	}

 	/**
@@ -384,13 +437,15 @@ class PushTokenRestController extends RestApiControllerBase {
 				'context'           => array( 'delete' ),
 				'minimum'           => 1,
 				'sanitize_callback' => 'absint',
+				'validate_callback' => array( $this, 'validate_argument' ),
 			),
 			'origin'      => array(
-				'description' => __( 'Origin', 'woocommerce' ),
-				'type'        => 'string',
-				'required'    => true,
-				'context'     => array( 'create' ),
-				'enum'        => PushToken::ORIGINS,
+				'description'       => __( 'Origin', 'woocommerce' ),
+				'type'              => 'string',
+				'required'          => true,
+				'context'           => array( 'create' ),
+				'enum'              => PushToken::ORIGINS,
+				'validate_callback' => array( $this, 'validate_argument' ),
 			),
 			'device_uuid' => array(
 				'description'       => __( 'Device UUID', 'woocommerce' ),
@@ -401,11 +456,12 @@ class PushTokenRestController extends RestApiControllerBase {
 				'sanitize_callback' => 'sanitize_text_field',
 			),
 			'platform'    => array(
-				'description' => __( 'Platform', 'woocommerce' ),
-				'type'        => 'string',
-				'required'    => true,
-				'context'     => array( 'create' ),
-				'enum'        => PushToken::PLATFORMS,
+				'description'       => __( 'Platform', 'woocommerce' ),
+				'type'              => 'string',
+				'required'          => true,
+				'context'           => array( 'create' ),
+				'enum'              => PushToken::PLATFORMS,
+				'validate_callback' => array( $this, 'validate_argument' ),
 			),
 			'token'       => array(
 				'description'       => __( 'Push Token', 'woocommerce' ),
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php b/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php
index 0cff271282..2207c9bac2 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php
@@ -10,9 +10,11 @@ namespace Automattic\WooCommerce\Internal\PushNotifications\DataStores;
 defined( 'ABSPATH' ) || exit;

 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
+use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenInvalidDataException;
 use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenNotFoundException;
 use Exception;
-use InvalidArgumentException;
+use WC_Data_Exception;
+use WP_Http;
 use WP_Query;

 /**
@@ -33,13 +35,13 @@ class PushTokensDataStore {
 	 *
 	 * @since 10.5.0
 	 * @param PushToken $push_token An instance of PushToken.
-	 * @throws InvalidArgumentException If the token can't be created.
-	 * @throws Exception If the token creation fails.
+	 * @throws PushTokenInvalidDataException If the token can't be created.
+	 * @throws WC_Data_Exception If the token creation fails.
 	 * @return void
 	 */
 	public function create( PushToken &$push_token ): void {
 		if ( ! $push_token->can_be_created() ) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				'Can\'t create push token because the push token data provided is invalid.'
 			);
 		}
@@ -55,8 +57,13 @@ class PushTokensDataStore {
 		);

 		if ( is_wp_error( $id ) ) {
-			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
-			throw new Exception( $id->get_error_message() );
+			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped
+			throw new WC_Data_Exception(
+				(string) $id->get_error_code(),
+				$id->get_error_message(),
+				WP_Http::INTERNAL_SERVER_ERROR
+			);
+			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
 		}

 		$push_token->set_id( $id );
@@ -67,13 +74,13 @@ class PushTokensDataStore {
 	 *
 	 * @since 10.5.0
 	 * @param PushToken $push_token An instance of PushToken.
-	 * @throws InvalidArgumentException If the token can't be read.
+	 * @throws PushTokenInvalidDataException If the token can't be read.
 	 * @throws PushTokenNotFoundException If the token can't be found.
 	 * @return void
 	 */
 	public function read( PushToken &$push_token ): void {
 		if ( ! $push_token->can_be_read() ) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				'Can\'t read push token because the push token data provided is invalid.'
 			);
 		}
@@ -95,7 +102,7 @@ class PushTokensDataStore {
 				&& PushToken::PLATFORM_BROWSER !== $meta['platform']
 			)
 		) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				'Can\'t read push token because the push token record is malformed.'
 			);
 		}
@@ -112,14 +119,14 @@ class PushTokensDataStore {
 	 *
 	 * @since 10.5.0
 	 * @param PushToken $push_token An instance of PushToken.
-	 * @throws InvalidArgumentException If the token can't be updated.
+	 * @throws PushTokenInvalidDataException If the token can't be updated.
 	 * @throws PushTokenNotFoundException If the token can't be found.
-	 * @throws Exception If the token update fails.
+	 * @throws WC_Data_Exception If the token update fails.
 	 * @return void
 	 */
 	public function update( PushToken &$push_token ): void {
 		if ( ! $push_token->can_be_updated() ) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				'Can\'t update push token because the push token data provided is invalid.'
 			);
 		}
@@ -142,8 +149,13 @@ class PushTokensDataStore {
 		);

 		if ( is_wp_error( $result ) ) {
-			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
-			throw new Exception( $result->get_error_message() );
+			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped
+			throw new WC_Data_Exception(
+				(string) $result->get_error_code(),
+				$result->get_error_message(),
+				WP_Http::INTERNAL_SERVER_ERROR
+			);
+			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
 		}

 		if ( null === $push_token->get_device_uuid() ) {
@@ -156,13 +168,13 @@ class PushTokensDataStore {
 	 *
 	 * @since 10.5.0
 	 * @param PushToken $push_token An instance of PushToken.
-	 * @throws InvalidArgumentException If the token can't be deleted.
+	 * @throws PushTokenInvalidDataException If the token can't be deleted.
 	 * @throws PushTokenNotFoundException If the token can't be found.
 	 * @return void
 	 */
 	public function delete( PushToken &$push_token ): void {
 		if ( ! $push_token->can_be_deleted() ) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				'Can\'t delete push token because the push token data provided is invalid.'
 			);
 		}
@@ -186,7 +198,7 @@ class PushTokensDataStore {
 	 * @since 10.5.0
 	 * @param PushToken $push_token An instance of PushToken.
 	 * @return null|PushToken
-	 * @throws InvalidArgumentException If push token is missing data.
+	 * @throws PushTokenInvalidDataException If push token is missing data.
 	 */
 	public function get_by_token_or_device_id( PushToken &$push_token ): ?PushToken {
 		if (
@@ -209,7 +221,7 @@ class PushTokensDataStore {
 				&& ! $push_token->get_token()
 			)
 		) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				'Can\'t retrieve push token because the push token data provided is invalid.'
 			);
 		}
@@ -277,11 +289,11 @@ class PushTokensDataStore {
 	 * @since 10.5.0
 	 * @param PushToken $push_token An instance of PushToken.
 	 * @return array
-	 * @throws InvalidArgumentException If the token can't be read.
+	 * @throws PushTokenInvalidDataException If the token can't be read.
 	 */
 	private function build_meta_array_from_database( PushToken &$push_token ) {
 		if ( ! $push_token->can_be_read() ) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				'Can\'t read meta for push token because the push token data provided is invalid.'
 			);
 		}
@@ -309,7 +321,6 @@ class PushTokensDataStore {
 	 * @since 10.5.0
 	 * @param PushToken $push_token An instance of PushToken.
 	 * @return array
-	 * @throws InvalidArgumentException If the token can't be read.
 	 */
 	private function build_meta_array_from_token( PushToken &$push_token ) {
 		return array_filter(
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Entities/PushToken.php b/plugins/woocommerce/src/Internal/PushNotifications/Entities/PushToken.php
index a1676b4e8c..409bfb0932 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Entities/PushToken.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Entities/PushToken.php
@@ -6,7 +6,7 @@ namespace Automattic\WooCommerce\Internal\PushNotifications\Entities;

 defined( 'ABSPATH' ) || exit;

-use InvalidArgumentException;
+use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenInvalidDataException;

 /**
  * Object representation of a push token.
@@ -141,7 +141,7 @@ class PushToken {
 	 * @param string|null $device_uuid The UUID of the device that generated the token.
 	 * @param string|null $platform    The platform the token was generated by.
 	 * @param string|null $origin      The origin the token belongs to.
-	 * @throws InvalidArgumentException If any of the provided values fail validation.
+	 * @throws PushTokenInvalidDataException If any of the provided values fail validation.
 	 * @return PushToken
 	 *
 	 * @since 10.4.0
@@ -187,14 +187,14 @@ class PushToken {
 	 * Sets the ID.
 	 *
 	 * @param int $id The id of the token post.
-	 * @throws InvalidArgumentException If ID is <= 0.
+	 * @throws PushTokenInvalidDataException If ID is <= 0.
 	 * @return void
 	 *
 	 * @since 10.4.0
 	 */
 	public function set_id( int $id ): void {
 		if ( $id <= 0 ) {
-			throw new InvalidArgumentException( 'ID must be a positive integer.' );
+			throw new PushTokenInvalidDataException( 'ID must be a positive integer.' );
 		}

 		$this->id = $id;
@@ -204,14 +204,14 @@ class PushToken {
 	 * Sets the user ID.
 	 *
 	 * @param int $user_id The id of the user who owns the token.
-	 * @throws InvalidArgumentException If ID is <= 0.
+	 * @throws PushTokenInvalidDataException If ID is <= 0.
 	 * @return void
 	 *
 	 * @since 10.4.0
 	 */
 	public function set_user_id( int $user_id ): void {
 		if ( $user_id <= 0 ) {
-			throw new InvalidArgumentException( 'User ID must be a positive integer.' );
+			throw new PushTokenInvalidDataException( 'User ID must be a positive integer.' );
 		}

 		$this->user_id = $user_id;
@@ -221,7 +221,7 @@ class PushToken {
 	 * Sets the token.
 	 *
 	 * @param string $token The token representing a device we can send a push notification to.
-	 * @throws InvalidArgumentException If token is empty or exceeds maximum length.
+	 * @throws PushTokenInvalidDataException If token is empty or exceeds maximum length.
 	 * @return void
 	 *
 	 * @since 10.4.0
@@ -230,11 +230,11 @@ class PushToken {
 		$token = trim( $token );

 		if ( '' === $token ) {
-			throw new InvalidArgumentException( 'Token cannot be empty.' );
+			throw new PushTokenInvalidDataException( 'Token cannot be empty.' );
 		}

 		if ( strlen( $token ) > self::MAX_TOKEN_LENGTH ) {
-			throw new InvalidArgumentException(
+			throw new PushTokenInvalidDataException(
 				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
 				sprintf( 'Token exceeds maximum length of %s.', self::MAX_TOKEN_LENGTH )
 			);
@@ -263,14 +263,14 @@ class PushToken {
 	 * Sets the platform.
 	 *
 	 * @param string $platform The platform the token was generated by.
-	 * @throws InvalidArgumentException If the platform is invalid.
+	 * @throws PushTokenInvalidDataException If the platform is invalid.
 	 * @return void
 	 *
 	 * @since 10.4.0
 	 */
 	public function set_platform( string $platform ): void {
 		if ( ! in_array( $platform, self::PLATFORMS, true ) ) {
-			throw new InvalidArgumentException( 'Platform for PushToken is invalid.' );
+			throw new PushTokenInvalidDataException( 'Platform for PushToken is invalid.' );
 		}

 		$this->platform = $platform;
@@ -280,14 +280,14 @@ class PushToken {
 	 * Sets the origin.
 	 *
 	 * @param string $origin The origin of the token, e.g. the app it came from.
-	 * @throws InvalidArgumentException If the origin is invalid.
+	 * @throws PushTokenInvalidDataException If the origin is invalid.
 	 * @return void
 	 *
 	 * @since 10.4.0
 	 */
 	public function set_origin( string $origin ): void {
 		if ( ! in_array( $origin, self::ORIGINS, true ) ) {
-			throw new InvalidArgumentException( 'Origin for PushToken is invalid.' );
+			throw new PushTokenInvalidDataException( 'Origin for PushToken is invalid.' );
 		}

 		$this->origin = $origin;
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Exceptions/PushTokenInvalidDataException.php b/plugins/woocommerce/src/Internal/PushNotifications/Exceptions/PushTokenInvalidDataException.php
new file mode 100644
index 0000000000..dfa9769f1e
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Exceptions/PushTokenInvalidDataException.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * PushTokenInvalidDataException class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\PushNotifications\Exceptions;
+
+defined( 'ABSPATH' ) || exit;
+
+use WC_Data_Exception;
+use WP_Http;
+
+/**
+ * Exception thrown when push token data is invalid.
+ *
+ * @since 10.6.0
+ */
+class PushTokenInvalidDataException extends WC_Data_Exception {
+	/**
+	 * Constructor.
+	 *
+	 * @since 10.6.0
+	 * @param string $message The validation error message.
+	 */
+	public function __construct( string $message ) {
+		parent::__construct(
+			'woocommerce_invalid_data',
+			$message,
+			WP_Http::BAD_REQUEST
+		);
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Exceptions/PushTokenNotFoundException.php b/plugins/woocommerce/src/Internal/PushNotifications/Exceptions/PushTokenNotFoundException.php
index 509fdc47f3..afc4cd7198 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Exceptions/PushTokenNotFoundException.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Exceptions/PushTokenNotFoundException.php
@@ -9,7 +9,7 @@ namespace Automattic\WooCommerce\Internal\PushNotifications\Exceptions;

 defined( 'ABSPATH' ) || exit;

-use Exception;
+use WC_Data_Exception;
 use WP_Http;

 /**
@@ -17,11 +17,17 @@ use WP_Http;
  *
  * @since 10.5.0
  */
-class PushTokenNotFoundException extends Exception {
+class PushTokenNotFoundException extends WC_Data_Exception {
 	/**
-	 * Default exception message.
+	 * Constructor.
 	 *
-	 * @var string
+	 * @since 10.6.0
 	 */
-	protected $message = 'Push token could not be found.';
+	public function __construct() {
+		parent::__construct(
+			'woocommerce_invalid_push_token',
+			'Push token could not be found.',
+			WP_Http::NOT_FOUND
+		);
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
index 626b8b2db1..ffc8360a5e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
@@ -9,14 +9,15 @@ use Automattic\WooCommerce\Internal\Features\FeaturesController;
 use Automattic\WooCommerce\Internal\PushNotifications\Controllers\PushTokenRestController;
 use Automattic\WooCommerce\Internal\PushNotifications\DataStores\PushTokensDataStore;
 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
+use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenInvalidDataException;
 use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenNotFoundException;
 use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
 use Exception;
-use InvalidArgumentException;
 use RuntimeException;
 use PHPUnit\Framework\MockObject\MockObject;
 use ReflectionClass;
+use WC_Data_Exception;
 use WC_REST_Unit_Test_Case;
 use WP_Error;
 use WP_Http;
@@ -762,7 +763,7 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {

 		$data = $response->get_data();

-		$this->assertEquals( 'woocommerce_rest_invalid_push_token', $data['code'] );
+		$this->assertEquals( 'woocommerce_invalid_push_token', $data['code'] );
 		$this->assertEquals( 'Push token could not be found.', $data['message'] );
 	}

@@ -782,7 +783,7 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {

 		$data = $response->get_data();

-		$this->assertEquals( 'woocommerce_rest_invalid_push_token', $data['code'] );
+		$this->assertEquals( 'woocommerce_invalid_push_token', $data['code'] );
 		$this->assertEquals( 'Push token could not be found.', $data['message'] );
 	}

@@ -957,7 +958,7 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
 		$result = $method->invoke( $controller, $exception );

 		$this->assertInstanceOf( WP_Error::class, $result );
-		$this->assertEquals( 'woocommerce_rest_internal_error', $result->get_error_code() );
+		$this->assertEquals( 'woocommerce_internal_error', $result->get_error_code() );
 		$this->assertEquals( 'Internal server error', $result->get_error_message() );
 		$this->assertEquals( WP_Http::INTERNAL_SERVER_ERROR, $result->get_error_data()['status'] );
 	}
@@ -977,18 +978,18 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
 		$result = $method->invoke( $controller, $exception );

 		$this->assertInstanceOf( WP_Error::class, $result );
-		$this->assertEquals( 'woocommerce_rest_invalid_push_token', $result->get_error_code() );
+		$this->assertEquals( 'woocommerce_invalid_push_token', $result->get_error_code() );
 		$this->assertEquals( 'Push token could not be found.', $result->get_error_message() );
 		$this->assertEquals( WP_Http::NOT_FOUND, $result->get_error_data()['status'] );
 	}

 	/**
 	 * @testdox Test convert_exception_to_wp_error exposes message for
-	 * InvalidArgumentException.
+	 * PushTokenInvalidDataException.
 	 */
-	public function test_it_exposes_message_for_invalid_argument_exception() {
+	public function test_it_exposes_message_for_push_token_invalid_data_exception() {
 		$controller = new PushTokenRestController();
-		$exception  = new InvalidArgumentException( 'Invalid argument provided.' );
+		$exception  = new PushTokenInvalidDataException( 'Invalid argument provided.' );

 		$reflection = new ReflectionClass( $controller );
 		$method     = $reflection->getMethod( 'convert_exception_to_wp_error' );
@@ -997,19 +998,46 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
 		$result = $method->invoke( $controller, $exception );

 		$this->assertInstanceOf( WP_Error::class, $result );
-		$this->assertEquals( 'woocommerce_rest_invalid_argument', $result->get_error_code() );
+		$this->assertEquals( 'woocommerce_invalid_data', $result->get_error_code() );
 		$this->assertEquals( 'Invalid argument provided.', $result->get_error_message() );
 		$this->assertEquals( WP_Http::BAD_REQUEST, $result->get_error_data()['status'] );
 	}

 	/**
-	 * @testdox Test convert_exception_to_wp_error hides message for unknown
-	 * exception subclasses.
+	 * @testdox Test convert_exception_to_wp_error correctly handles any non-500
+	 * WC_Data_Exception.
 	 */
-	public function test_it_hides_internal_error_message_for_unknown_exception_subclass() {
+	public function test_it_handles_any_non_500_wc_data_exception() {
 		$controller = new PushTokenRestController();
-		// RuntimeException is a subclass of Exception but not in our mapping.
-		$exception = new RuntimeException( 'Sensitive runtime error details' );
+		$exception  = new WC_Data_Exception(
+			'custom_error_code',
+			'Custom error message.',
+			WP_Http::FORBIDDEN
+		);
+
+		$reflection = new ReflectionClass( $controller );
+		$method     = $reflection->getMethod( 'convert_exception_to_wp_error' );
+		$method->setAccessible( true );
+
+		$result = $method->invoke( $controller, $exception );
+
+		$this->assertInstanceOf( WP_Error::class, $result );
+		$this->assertEquals( 'custom_error_code', $result->get_error_code() );
+		$this->assertEquals( 'Custom error message.', $result->get_error_message() );
+		$this->assertEquals( WP_Http::FORBIDDEN, $result->get_error_data()['status'] );
+	}
+
+	/**
+	 * @testdox Test convert_exception_to_wp_error correctly handles a 500
+	 * WC_Data_Exception.
+	 */
+	public function test_it_handles_a_500_wc_data_exception() {
+		$controller = new PushTokenRestController();
+		$exception  = new WC_Data_Exception(
+			'custom_error_code',
+			'Custom error message.',
+			WP_Http::INTERNAL_SERVER_ERROR
+		);

 		$reflection = new ReflectionClass( $controller );
 		$method     = $reflection->getMethod( 'convert_exception_to_wp_error' );
@@ -1018,7 +1046,7 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
 		$result = $method->invoke( $controller, $exception );

 		$this->assertInstanceOf( WP_Error::class, $result );
-		$this->assertEquals( 'woocommerce_rest_internal_error', $result->get_error_code() );
+		$this->assertEquals( 'woocommerce_internal_error', $result->get_error_code() );
 		$this->assertEquals( 'Internal server error', $result->get_error_message() );
 		$this->assertEquals( WP_Http::INTERNAL_SERVER_ERROR, $result->get_error_data()['status'] );
 	}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php
index 3b17c9568f..a02b31d27f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php
@@ -6,8 +6,8 @@ namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\DataStores;

 use Automattic\WooCommerce\Internal\PushNotifications\DataStores\PushTokensDataStore;
 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
-use Exception;
-use InvalidArgumentException;
+use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenInvalidDataException;
+use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenNotFoundException;
 use WC_Unit_Test_Case;

 /**
@@ -142,7 +142,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_user_id( 1 );
 		$push_token->set_token( 'test_token' );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t create push token because the push token data provided is invalid.' );

 		$data_store->create( $push_token );
@@ -157,7 +157,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {

 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t read push token because the push token data provided is invalid.' );

 		$data_store->read( $push_token );
@@ -173,7 +173,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token = new PushToken();
 		$push_token->set_id( 999999 );

-		$this->expectException( Exception::class );
+		$this->expectException( PushTokenNotFoundException::class );
 		$this->expectExceptionMessage( 'Push token could not be found.' );

 		$data_store->read( $push_token );
@@ -197,7 +197,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token = new PushToken();
 		$push_token->set_id( $post_id );

-		$this->expectException( Exception::class );
+		$this->expectException( PushTokenNotFoundException::class );
 		$this->expectExceptionMessage( 'Push token could not be found.' );

 		$data_store->read( $push_token );
@@ -226,7 +226,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token = new PushToken();
 		$push_token->set_id( $post_id );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t read push token because the push token record is malformed.' );

 		$data_store->read( $push_token );
@@ -243,7 +243,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_id( 1 );
 		$push_token->set_user_id( 1 );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t update push token because the push token data provided is invalid.' );

 		$data_store->update( $push_token );
@@ -264,7 +264,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_device_uuid( 'device-uuid' );
 		$push_token->set_origin( PushToken::ORIGIN_WOOCOMMERCE_IOS );

-		$this->expectException( Exception::class );
+		$this->expectException( PushTokenNotFoundException::class );
 		$this->expectExceptionMessage( 'Push token could not be found.' );

 		$data_store->update( $push_token );
@@ -293,7 +293,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_device_uuid( 'device-uuid' );
 		$push_token->set_origin( PushToken::ORIGIN_WOOCOMMERCE_IOS );

-		$this->expectException( Exception::class );
+		$this->expectException( PushTokenNotFoundException::class );
 		$this->expectExceptionMessage( 'Push token could not be found.' );

 		$data_store->update( $push_token );
@@ -307,7 +307,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$data_store = new PushTokensDataStore();
 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t delete push token because the push token data provided is invalid.' );

 		$data_store->delete( $push_token );
@@ -331,7 +331,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token = new PushToken();
 		$push_token->set_id( $post_id );

-		$this->expectException( Exception::class );
+		$this->expectException( PushTokenNotFoundException::class );
 		$this->expectExceptionMessage( 'Push token could not be found.' );

 		$data_store->delete( $push_token );
@@ -551,7 +551,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_token( 'test_token' );
 		$push_token->set_device_uuid( 'test_device' );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t retrieve push token because the push token data provided is invalid.' );

 		$data_store->get_by_token_or_device_id( $push_token );
@@ -570,7 +570,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_token( 'test_token' );
 		$push_token->set_device_uuid( 'test_device' );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t retrieve push token because the push token data provided is invalid.' );

 		$data_store->get_by_token_or_device_id( $push_token );
@@ -589,7 +589,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_token( 'test_token' );
 		$push_token->set_device_uuid( 'test_device' );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t retrieve push token because the push token data provided is invalid.' );

 		$data_store->get_by_token_or_device_id( $push_token );
@@ -607,7 +607,7 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
 		$push_token->set_platform( PushToken::PLATFORM_APPLE );
 		$push_token->set_origin( PushToken::ORIGIN_WOOCOMMERCE_IOS );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Can\'t retrieve push token because the push token data provided is invalid.' );

 		$data_store->get_by_token_or_device_id( $push_token );
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Entities/PushTokenTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Entities/PushTokenTest.php
index 88cd7d11da..cd09e876de 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Entities/PushTokenTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Entities/PushTokenTest.php
@@ -5,7 +5,7 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Entities;

 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
-use InvalidArgumentException;
+use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenInvalidDataException;
 use WC_Unit_Test_Case;

 /**
@@ -388,7 +388,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 	public function test_it_throws_exception_when_setting_invalid_platform() {
 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Platform for PushToken is invalid.' );

 		$push_token->set_platform( 'invalid' );
@@ -400,7 +400,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 	public function test_it_throws_exception_when_setting_invalid_origin() {
 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Origin for PushToken is invalid.' );

 		$push_token->set_origin( 'com.invalid.app' );
@@ -431,7 +431,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 	public function test_it_throws_exception_when_setting_user_id_to_zero() {
 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'User ID must be a positive integer.' );

 		$push_token->set_user_id( 0 );
@@ -443,7 +443,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 	public function test_it_throws_exception_when_setting_negative_user_id() {
 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'User ID must be a positive integer.' );

 		$push_token->set_user_id( -1 );
@@ -455,7 +455,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 	public function test_it_throws_exception_when_setting_empty_token() {
 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Token cannot be empty.' );

 		$push_token->set_token( '' );
@@ -467,7 +467,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 	public function test_it_throws_exception_when_setting_whitespace_only_token() {
 		$push_token = new PushToken();

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Token cannot be empty.' );

 		$push_token->set_token( '   ' );
@@ -480,7 +480,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 		$push_token = new PushToken();
 		$long_token = str_repeat( 'A', PushToken::MAX_TOKEN_LENGTH + 1 );

-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Token exceeds maximum length of 4096.' );

 		$push_token->set_token( $long_token );
@@ -573,7 +573,7 @@ class PushTokenTest extends WC_Unit_Test_Case {
 	 * @testdox Tests get_new_instance throws exception for invalid values.
 	 */
 	public function test_get_new_instance_throws_exception_for_invalid_platform() {
-		$this->expectException( InvalidArgumentException::class );
+		$this->expectException( PushTokenInvalidDataException::class );
 		$this->expectExceptionMessage( 'Platform for PushToken is invalid.' );

 		PushToken::get_new_instance(