Commit 678191ad9cf for woocommerce

commit 678191ad9cfb4d8243da1859e7434693f6774354
Author: Sergey Mitroshin <sergeymitr@gmail.com>
Date:   Thu Nov 5 19:49:41 2026 -0500

    Move UCP Store API utilities into their own namespace (#70726)

    Move UCP Store API utilities into their own namespace.

diff --git a/plugins/woocommerce/changelog/refactor-ucp-utilities-namespace b/plugins/woocommerce/changelog/refactor-ucp-utilities-namespace
new file mode 100644
index 00000000000..f224aad3a94
--- /dev/null
+++ b/plugins/woocommerce/changelog/refactor-ucp-utilities-namespace
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Move the UCP Store API utility classes into their own namespace and drop the redundant prefix; no merchant-facing change.
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index ed5fcd99d31..d65f707e473 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -680,6 +680,13 @@ final class WooCommerce {
 	/**
 	 * Returns true if the request is a store REST API or UCP API request.
 	 *
+	 * The UCP namespace counts as one: like the Store API it returns data rather than rendered
+	 * pages, so callers can skip the same work — block and template registration, site tracking.
+	 *
+	 * This is a rendering decision only. StoreApi\Authentication keeps its own, narrower check
+	 * and does not treat UCP as Store API: its filters assume a browser client with a nonce,
+	 * which a UCP agent never sends, so UCP routes must not inherit them.
+	 *
 	 * @return bool
 	 */
 	public function is_store_api_request() {
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php b/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php
index a5acb976260..2a293b1fa8f 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php
@@ -4,7 +4,7 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Internal\AgentReadiness;

 use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonHelper;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 defined( 'ABSPATH' ) || exit;

@@ -64,7 +64,7 @@ class StoreConditions {
 			),
 			array(
 				'id'     => 'ucp_on',
-				'ok'     => UcpUtils::is_enabled(),
+				'ok'     => Utils::is_enabled(),
 				'blocks' => 'ucp',
 				'label'  => __( 'Universal Commerce Protocol (UCP) is off', 'woocommerce' ),
 				'why'    => __( 'Shopping agents that use UCP cannot read your catalog.', 'woocommerce' ),
diff --git a/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php b/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
index 89d293d6642..f100be59b78 100644
--- a/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
+++ b/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
@@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Internal\Ucp;

 use Automattic\WooCommerce\Internal\Features\FeaturesController;
 use Automattic\WooCommerce\Internal\RegisterHooksInterface;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * Serves the UCP discovery profile at `/.well-known/ucp`.
@@ -38,7 +38,7 @@ final class UcpDiscovery implements RegisterHooksInterface {
 	public function register_feature_hooks(): void {
 		global $wp_rewrite;

-		$active = UcpUtils::is_enabled() && $wp_rewrite->using_permalinks();
+		$active = Utils::is_enabled() && $wp_rewrite->using_permalinks();

 		if ( $active ) {
 			add_rewrite_rule( self::RULE, 'index.php?' . self::QUERY_VAR . '=1', 'top' );
@@ -71,7 +71,7 @@ final class UcpDiscovery implements RegisterHooksInterface {
 	 * @param string $feature_id Feature being toggled.
 	 */
 	public function maybe_flush_rewrite_rules( string $feature_id ): void {
-		if ( UcpUtils::FEATURE_ID === $feature_id ) {
+		if ( Utils::FEATURE_ID === $feature_id ) {
 			update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' );
 		}
 	}
@@ -111,15 +111,15 @@ final class UcpDiscovery implements RegisterHooksInterface {
 	public function get_profile(): array {
 		return array(
 			'ucp'   => array(
-				'version'      => UcpUtils::VERSION,
+				'version'      => Utils::VERSION,
 				'services'     => array(
 					'dev.ucp.shopping' => array(
 						array(
-							'version'   => UcpUtils::VERSION,
+							'version'   => Utils::VERSION,
 							'transport' => 'rest',
 							'endpoint'  => rest_url( 'wc/ucp/v1' ),
-							'spec'      => 'https://ucp.dev/' . UcpUtils::VERSION . '/specification/overview',
-							'schema'    => 'https://ucp.dev/' . UcpUtils::VERSION . '/services/shopping/rest.openapi.json',
+							'spec'      => 'https://ucp.dev/' . Utils::VERSION . '/specification/overview',
+							'schema'    => 'https://ucp.dev/' . Utils::VERSION . '/services/shopping/rest.openapi.json',
 						),
 					),
 				),
@@ -127,7 +127,7 @@ final class UcpDiscovery implements RegisterHooksInterface {
 					static function ( array $declaration ): array {
 						return array( $declaration );
 					},
-					UcpUtils::capability_declarations()
+					Utils::capability_declarations()
 				),
 			),
 			'links' => array(
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
index 344710b91f3..6d3fed42d56 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
@@ -7,8 +7,8 @@ use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute as StoreApiCartR
 use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
 use Automattic\WooCommerce\StoreApi\SessionHandler;
 use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpCartMapper;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\CartMapper;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * Shared behaviour of the UCP cart routes.
@@ -51,7 +51,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
 	 * @return array
 	 */
 	protected function ucp_metadata( string $status ): array {
-		return UcpUtils::response_metadata( UcpUtils::CAPABILITY_CART, $status );
+		return Utils::response_metadata( Utils::CAPABILITY_CART, $status );
 	}

 	/**
@@ -67,7 +67,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
 	 * @return \WP_REST_Response
 	 */
 	public function get_response( \WP_REST_Request $request ) {
-		$refusal = UcpUtils::refuse_non_json_payload( $request, UcpUtils::CAPABILITY_CART );
+		$refusal = Utils::refuse_non_json_payload( $request, Utils::CAPABILITY_CART );
 		if ( null !== $refusal ) {
 			return $this->add_response_headers( $refusal );
 		}
@@ -297,7 +297,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
 		return new \WP_REST_Response(
 			array_merge(
 				array( 'ucp' => $this->ucp_metadata( 'success' ) ),
-				( new UcpCartMapper() )->build_cart( $this->cart_item_response(), $cart_id, $context )
+				( new CartMapper() )->build_cart( $this->cart_item_response(), $cart_id, $context )
 			),
 			200
 		);
@@ -328,7 +328,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function ucp_error( string $code, string $message, string $severity, int $status, string $path = '$' ): \WP_REST_Response {
-		return UcpUtils::error_response( UcpUtils::CAPABILITY_CART, $code, $message, $severity, $status, $path );
+		return Utils::error_response( Utils::CAPABILITY_CART, $code, $message, $severity, $status, $path );
 	}

 	/**
@@ -349,7 +349,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
 		$message = (string) $error->get_error_message();

 		return $this->ucp_error(
-			UcpUtils::cart_error_code( (string) $error->get_error_code(), 'TRANSPORT_ERROR' ),
+			Utils::cart_error_code( (string) $error->get_error_code(), 'TRANSPORT_ERROR' ),
 			'' !== $message ? $message : __( 'The cart could not be loaded. Please try again.', 'woocommerce' ),
 			$status >= 500 ? 'unrecoverable' : 'recoverable',
 			$status
@@ -365,7 +365,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function ucp_not_found(): \WP_REST_Response {
-		return UcpUtils::error_response( UcpUtils::CAPABILITY_CART, 'not_found', __( 'Cart not found.', 'woocommerce' ), 'unrecoverable', 200, '$.id' );
+		return Utils::error_response( Utils::CAPABILITY_CART, 'not_found', __( 'Cart not found.', 'woocommerce' ), 'unrecoverable', 200, '$.id' );
 	}

 	/**
@@ -503,7 +503,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
 			}

 			return $this->ucp_error(
-				UcpUtils::cart_error_code( $error->getErrorCode() ),
+				Utils::cart_error_code( $error->getErrorCode() ),
 				$error->getMessage(),
 				'recoverable',
 				200,
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php
index f4313823ae2..ed6a3b4b394 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php
@@ -6,7 +6,7 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
 use Automattic\WooCommerce\Internal\ProductFeed\Mapping\UcpProductMapper;
 use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
 use Automattic\WooCommerce\StoreApi\Schemas\V1\Ucp\UcpProductSchema;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * Shared behaviour of the UCP catalog routes.
@@ -27,7 +27,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
 	private $mapper;

 	/**
-	 * The UCP capability this route serves, one of the UcpUtils::CAPABILITY_* constants.
+	 * The UCP capability this route serves, one of the Utils::CAPABILITY_* constants.
 	 *
 	 * @return string
 	 */
@@ -40,7 +40,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
 	 * @return array
 	 */
 	protected function ucp_metadata( string $status ): array {
-		return UcpUtils::response_metadata( $this->get_capability(), $status );
+		return Utils::response_metadata( $this->get_capability(), $status );
 	}

 	/**
@@ -70,7 +70,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
 	 * @return \WP_REST_Response
 	 */
 	public function get_response( \WP_REST_Request $request ) {
-		return UcpUtils::refuse_non_json_payload( $request, $this->get_capability() ) ?? parent::get_response( $request );
+		return Utils::refuse_non_json_payload( $request, $this->get_capability() ) ?? parent::get_response( $request );
 	}

 	/**
@@ -125,7 +125,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
 	 * @return array
 	 */
 	protected function get_context( \WP_REST_Request $request ): array {
-		$raw = UcpUtils::json_param( $request, 'context' );
+		$raw = Utils::json_param( $request, 'context' );

 		if ( ! is_array( $raw ) ) {
 			return array();
@@ -152,7 +152,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function ucp_error( string $code, string $message, string $severity, int $status ): \WP_REST_Response {
-		return UcpUtils::error_response( $this->get_capability(), $code, $message, $severity, $status );
+		return Utils::error_response( $this->get_capability(), $code, $message, $severity, $status );
 	}

 	/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php
index 7ef51f3d15e..fee2b5d5f8d 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;

-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * UCP `POST /carts` route.
@@ -69,7 +69,7 @@ class Carts extends AbstractCartRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function get_route_post_response( \WP_REST_Request $request ) {
-		$line_items = UcpUtils::json_param( $request, 'line_items' );
+		$line_items = Utils::json_param( $request, 'line_items' );

 		if ( null !== $line_items ) {
 			$desired = $this->parse_line_items( $line_items );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php
index 31eade8ffa1..a1d26f90bbe 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;

-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * UCP `GET /carts/{id}` and `PUT /carts/{id}` route.
@@ -88,7 +88,7 @@ class CartsById extends AbstractCartRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function get_route_update_response( \WP_REST_Request $request ) {
-		$line_items = UcpUtils::json_param( $request, 'line_items' );
+		$line_items = Utils::json_param( $request, 'line_items' );

 		// `line_items` is the whole desired state, so omitting it is not the same as
 		// sending an empty list: it leaves the cart alone.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php
index 5bf69f88559..f06cbf58cde 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;

-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * UCP `POST /catalog/lookup` route.
@@ -50,7 +50,7 @@ class CatalogLookup extends AbstractCatalogRoute {
 	 * @return string
 	 */
 	protected function get_capability(): string {
-		return UcpUtils::CAPABILITY_CATALOG_LOOKUP;
+		return Utils::CAPABILITY_CATALOG_LOOKUP;
 	}

 	/**
@@ -84,7 +84,7 @@ class CatalogLookup extends AbstractCatalogRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function get_route_post_response( \WP_REST_Request $request ) {
-		$raw_ids = UcpUtils::json_param( $request, 'ids' );
+		$raw_ids = Utils::json_param( $request, 'ids' );

 		if ( ! is_array( $raw_ids ) || empty( $raw_ids ) ) {
 			return $this->ucp_error( 'VALIDATION_ERROR', __( 'ids is required.', 'woocommerce' ), 'recoverable', 400 );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php
index dddeb507e41..5045627159a 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;

-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * UCP `POST /catalog/product` route.
@@ -47,7 +47,7 @@ class CatalogProduct extends AbstractCatalogRoute {
 	 * @return string
 	 */
 	protected function get_capability(): string {
-		return UcpUtils::CAPABILITY_CATALOG_LOOKUP;
+		return Utils::CAPABILITY_CATALOG_LOOKUP;
 	}

 	/**
@@ -84,13 +84,13 @@ class CatalogProduct extends AbstractCatalogRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function get_route_post_response( \WP_REST_Request $request ) {
-		$id = trim( strval( UcpUtils::json_param( $request, 'id' ) ) );
+		$id = trim( strval( Utils::json_param( $request, 'id' ) ) );

 		if ( '' === $id ) {
 			return $this->ucp_error( 'VALIDATION_ERROR', __( 'id is required.', 'woocommerce' ), 'recoverable', 400 );
 		}

-		$selected = (array) ( UcpUtils::json_param( $request, 'selected' ) ?? array() );
+		$selected = (array) ( Utils::json_param( $request, 'selected' ) ?? array() );

 		// Checked before the product is loaded: an over-long selection is refused
 		// without paying for the lookup it would have been applied to.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php
index 22764fd3274..c747164550c 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php
@@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;

 use Automattic\WooCommerce\Enums\CatalogVisibility;
 use Automattic\WooCommerce\StoreApi\Utilities\ProductQuery;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * UCP `POST /catalog/search` route.
@@ -57,7 +57,7 @@ class CatalogSearch extends AbstractCatalogRoute {
 	 * @return string
 	 */
 	protected function get_capability(): string {
-		return UcpUtils::CAPABILITY_CATALOG_SEARCH;
+		return Utils::CAPABILITY_CATALOG_SEARCH;
 	}

 	/**
@@ -99,9 +99,9 @@ class CatalogSearch extends AbstractCatalogRoute {
 	 * @return \WP_REST_Response
 	 */
 	protected function get_route_post_response( \WP_REST_Request $request ) {
-		$query      = trim( (string) UcpUtils::json_param( $request, 'query' ) );
-		$filters    = (array) ( UcpUtils::json_param( $request, 'filters' ) ?? array() );
-		$pagination = (array) ( UcpUtils::json_param( $request, 'pagination' ) ?? array() );
+		$query      = trim( (string) Utils::json_param( $request, 'query' ) );
+		$filters    = (array) ( Utils::json_param( $request, 'filters' ) ?? array() );
+		$pagination = (array) ( Utils::json_param( $request, 'pagination' ) ?? array() );
 		$context    = $this->get_context( $request );

 		$invalid = $this->validate_search_input( $query, $filters );
diff --git a/plugins/woocommerce/src/StoreApi/RoutesController.php b/plugins/woocommerce/src/StoreApi/RoutesController.php
index 8a57702430f..c854eb4d06a 100644
--- a/plugins/woocommerce/src/StoreApi/RoutesController.php
+++ b/plugins/woocommerce/src/StoreApi/RoutesController.php
@@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\StoreApi;

 use Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController;
 use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * RoutesController class.
@@ -84,7 +84,7 @@ class RoutesController {
 				Routes\V1\ShopperListItemsByKey::IDENTIFIER => Routes\V1\ShopperListItemsByKey::class,
 			],
 			'ucp'           => [
-				// Universal Commerce Protocol endpoints, gated by UcpUtils::is_enabled().
+				// Universal Commerce Protocol endpoints, gated by Utils::is_enabled().
 				Routes\V1\Ucp\CatalogSearch::IDENTIFIER  => Routes\V1\Ucp\CatalogSearch::class,
 				Routes\V1\Ucp\CatalogLookup::IDENTIFIER  => Routes\V1\Ucp\CatalogLookup::class,
 				Routes\V1\Ucp\CatalogProduct::IDENTIFIER => Routes\V1\Ucp\CatalogProduct::class,
@@ -107,7 +107,7 @@ class RoutesController {
 			$this->register_routes( 'shopper_lists', self::$api_namespace . '/v1' );
 		}

-		if ( UcpUtils::is_enabled() ) {
+		if ( Utils::is_enabled() ) {
 			$this->register_routes( 'ucp', 'wc/ucp/v1' );
 		}
 	}
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/UcpCartMapper.php b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/CartMapper.php
similarity index 96%
rename from plugins/woocommerce/src/StoreApi/Utilities/UcpCartMapper.php
rename to plugins/woocommerce/src/StoreApi/Utilities/Ucp/CartMapper.php
index 8e03356aa8d..ae7a5e03522 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/UcpCartMapper.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/CartMapper.php
@@ -1,7 +1,9 @@
 <?php
 declare( strict_types = 1 );

-namespace Automattic\WooCommerce\StoreApi\Utilities;
+namespace Automattic\WooCommerce\StoreApi\Utilities\Ucp;
+
+use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;

 /**
  * Maps a Store API cart response onto the UCP cart shape.
@@ -11,7 +13,7 @@ namespace Automattic\WooCommerce\StoreApi\Utilities;
  * storefront. Stateless; the catalog's {@see \Automattic\WooCommerce\Internal\ProductFeed\Mapping\UcpProductMapper}
  * is the same idea for products.
  */
-class UcpCartMapper {
+class CartMapper {
 	/**
 	 * Build the UCP cart payload.
 	 *
@@ -168,7 +170,7 @@ class UcpCartMapper {
 		foreach ( $errors as $error ) {
 			$messages[] = array(
 				'type'         => 'warning',
-				'code'         => UcpUtils::cart_error_code( (string) ( $error['code'] ?? '' ) ),
+				'code'         => Utils::cart_error_code( (string) ( $error['code'] ?? '' ) ),
 				'content'      => (string) ( $error['message'] ?? '' ),
 				'severity'     => 'recoverable',
 				'path'         => '$.line_items',
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/Ucp/Utils.php b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/Utils.php
new file mode 100644
index 00000000000..72418f31c89
--- /dev/null
+++ b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/Utils.php
@@ -0,0 +1,206 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\StoreApi\Utilities\Ucp;
+
+use Automattic\WooCommerce\Utilities\FeaturesUtil;
+
+/**
+ * Shared helpers for the UCP (Universal Commerce Protocol) routes.
+ */
+class Utils {
+	/**
+	 * UCP specification version these routes implement.
+	 */
+	const VERSION = '2026-04-08';
+
+	/**
+	 * Feature id gating the UCP routes, stored as `woocommerce_feature_ucp_enabled`.
+	 */
+	const FEATURE_ID = 'ucp';
+
+	/**
+	 * Capability served by `POST /catalog/search`.
+	 */
+	const CAPABILITY_CATALOG_SEARCH = 'dev.ucp.shopping.catalog.search';
+
+	/**
+	 * Capability served by `POST /catalog/lookup` and `POST /catalog/product`.
+	 */
+	const CAPABILITY_CATALOG_LOOKUP = 'dev.ucp.shopping.catalog.lookup';
+
+	/**
+	 * Capability served by the `/carts` routes.
+	 */
+	const CAPABILITY_CART = 'dev.ucp.shopping.cart';
+
+	/**
+	 * Whether the UCP routes are registered.
+	 *
+	 * @return bool
+	 */
+	public static function is_enabled(): bool {
+		return FeaturesUtil::feature_is_enabled( self::FEATURE_ID );
+	}
+
+	/**
+	 * Build the `ucp` envelope every UCP response carries.
+	 *
+	 * UCP requires `capabilities` to name only the capability relevant to the
+	 * operation being answered, so each response declares exactly one.
+	 *
+	 * @param string $capability One of the CAPABILITY_* constants.
+	 * @param string $status Response status, `success` or `error`.
+	 * @return array
+	 */
+	public static function response_metadata( string $capability, string $status ): array {
+		return array(
+			'version'      => self::VERSION,
+			'status'       => $status,
+			'capabilities' => array(
+				$capability => array( self::capability_declaration( $capability ) ),
+			),
+		);
+	}
+
+	/**
+	 * Spec and schema references for every capability these routes serve, keyed by capability name.
+	 *
+	 * @return array
+	 */
+	public static function capability_declarations(): array {
+		return array(
+			self::CAPABILITY_CATALOG_SEARCH => array(
+				'version' => self::VERSION,
+				'spec'    => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/search',
+				'schema'  => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_search.json',
+			),
+			self::CAPABILITY_CATALOG_LOOKUP => array(
+				'version' => self::VERSION,
+				'spec'    => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/lookup',
+				'schema'  => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_lookup.json',
+			),
+			self::CAPABILITY_CART           => array(
+				'version' => self::VERSION,
+				'spec'    => 'https://ucp.dev/' . self::VERSION . '/specification/cart',
+				'schema'  => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/cart.json',
+			),
+		);
+	}
+
+	/**
+	 * Spec and schema references for a capability.
+	 *
+	 * @param string $capability One of the CAPABILITY_* constants.
+	 * @return array
+	 */
+	private static function capability_declaration( string $capability ): array {
+		return self::capability_declarations()[ $capability ] ?? array( 'version' => self::VERSION );
+	}
+
+	/**
+	 * Build a UCP error response.
+	 *
+	 * The envelope shape every route shares. `$status` follows the UCP split: a malformed
+	 * request keeps a non-2xx status, a valid request that ends in a refusal is a business
+	 * outcome reported at 200 through `ucp.status` and `messages`.
+	 *
+	 * @param string $capability One of the CAPABILITY_* constants.
+	 * @param string $code Error code.
+	 * @param string $message Error message.
+	 * @param string $severity Error severity.
+	 * @param int    $status HTTP status.
+	 * @param string $path JSONPath of the offending field.
+	 * @return \WP_REST_Response
+	 */
+	public static function error_response( string $capability, string $code, string $message, string $severity, int $status, string $path = '$' ): \WP_REST_Response {
+		return new \WP_REST_Response(
+			array(
+				'ucp'      => self::response_metadata( $capability, 'error' ),
+				'messages' => array(
+					array(
+						'type'         => 'error',
+						'code'         => $code,
+						'content'      => $message,
+						'severity'     => $severity,
+						'path'         => $path,
+						'content_type' => 'plain',
+					),
+				),
+			),
+			$status
+		);
+	}
+
+	/**
+	 * Refuse a request that brought a body or form fields under anything other than a JSON content type.
+	 *
+	 * A UCP request is a JSON document, so a form or query payload is never read in its
+	 * place. A bare request, such as a `cancel`, carries nothing and is not refused.
+	 *
+	 * @param \WP_REST_Request $request Request object.
+	 * @param string           $capability One of the CAPABILITY_* constants.
+	 *
+	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
+	 *
+	 * @return \WP_REST_Response|null The 415 refusal, or null when the request may be read.
+	 */
+	public static function refuse_non_json_payload( \WP_REST_Request $request, string $capability ): ?\WP_REST_Response {
+		if ( $request->is_json_content_type() ) {
+			return null;
+		}
+
+		if ( '' === trim( (string) $request->get_body() ) && array() === $request->get_body_params() ) {
+			return null;
+		}
+
+		return self::error_response( $capability, 'VALIDATION_ERROR', __( 'UCP requests must be sent as application/json.', 'woocommerce' ), 'recoverable', 415 );
+	}
+
+	/**
+	 * Read a parameter from the JSON document alone.
+	 *
+	 * `get_param()` would also resolve it from the form body or the query string. UCP defines
+	 * the request as the document, so nothing carried elsewhere is allowed to act.
+	 *
+	 * @param \WP_REST_Request $request Request object.
+	 * @param string           $name Parameter name.
+	 *
+	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
+	 *
+	 * @return mixed|null The value, or null when the document has none.
+	 */
+	public static function json_param( \WP_REST_Request $request, string $name ) {
+		$params = $request->get_json_params();
+
+		return is_array( $params ) && array_key_exists( $name, $params ) ? $params[ $name ] : null;
+	}
+
+	/**
+	 * Translate a cart error code into the UCP code closest to it.
+	 *
+	 * The cart names more reasons than UCP does, so the ones an agent can act on
+	 * differently — retry smaller, fix the quantity, or re-read the cart — stay apart
+	 * and the rest collapse.
+	 *
+	 * @param string $code Store API error code.
+	 * @param string $fallback UCP code for a cart error UCP has no closer name for.
+	 * @return string One of `out_of_stock`, `not_found`, `invalid_quantity` or the fallback.
+	 */
+	public static function cart_error_code( string $code, string $fallback = 'item_unavailable' ): string {
+		switch ( $code ) {
+			case 'woocommerce_rest_product_out_of_stock':
+			case 'woocommerce_rest_product_partially_out_of_stock':
+			case 'woocommerce_rest_product_too_many_in_cart':
+				return 'out_of_stock';
+			case 'woocommerce_rest_cart_invalid_key':
+				return 'not_found';
+			case 'woocommerce_rest_product_invalid_quantity':
+			case 'invalid_quantity':
+			case 'readonly_quantity':
+				return 'invalid_quantity';
+			default:
+				return $fallback;
+		}
+	}
+}
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php b/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php
index 70b142d40d9..105c824a87c 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php
@@ -3,204 +3,13 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\StoreApi\Utilities;

-use Automattic\WooCommerce\Utilities\FeaturesUtil;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
- * Shared helpers for the UCP (Universal Commerce Protocol) routes.
+ * Former name of the UCP helpers, kept so code written against it keeps loading.
+ *
+ * @deprecated since 11.4.0, use \Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils instead.
+ * @see Utils
  */
-class UcpUtils {
-	/**
-	 * UCP specification version these routes implement.
-	 */
-	const VERSION = '2026-04-08';
-
-	/**
-	 * Feature id gating the UCP routes, stored as `woocommerce_feature_ucp_enabled`.
-	 */
-	const FEATURE_ID = 'ucp';
-
-	/**
-	 * Capability served by `POST /catalog/search`.
-	 */
-	const CAPABILITY_CATALOG_SEARCH = 'dev.ucp.shopping.catalog.search';
-
-	/**
-	 * Capability served by `POST /catalog/lookup` and `POST /catalog/product`.
-	 */
-	const CAPABILITY_CATALOG_LOOKUP = 'dev.ucp.shopping.catalog.lookup';
-
-	/**
-	 * Capability served by the `/carts` routes.
-	 */
-	const CAPABILITY_CART = 'dev.ucp.shopping.cart';
-
-	/**
-	 * Whether the UCP routes are registered.
-	 *
-	 * @return bool
-	 */
-	public static function is_enabled(): bool {
-		return FeaturesUtil::feature_is_enabled( self::FEATURE_ID );
-	}
-
-	/**
-	 * Build the `ucp` envelope every UCP response carries.
-	 *
-	 * UCP requires `capabilities` to name only the capability relevant to the
-	 * operation being answered, so each response declares exactly one.
-	 *
-	 * @param string $capability One of the CAPABILITY_* constants.
-	 * @param string $status Response status, `success` or `error`.
-	 * @return array
-	 */
-	public static function response_metadata( string $capability, string $status ): array {
-		return array(
-			'version'      => self::VERSION,
-			'status'       => $status,
-			'capabilities' => array(
-				$capability => array( self::capability_declaration( $capability ) ),
-			),
-		);
-	}
-
-	/**
-	 * Spec and schema references for every capability these routes serve, keyed by capability name.
-	 *
-	 * @return array
-	 */
-	public static function capability_declarations(): array {
-		return array(
-			self::CAPABILITY_CATALOG_SEARCH => array(
-				'version' => self::VERSION,
-				'spec'    => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/search',
-				'schema'  => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_search.json',
-			),
-			self::CAPABILITY_CATALOG_LOOKUP => array(
-				'version' => self::VERSION,
-				'spec'    => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/lookup',
-				'schema'  => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_lookup.json',
-			),
-			self::CAPABILITY_CART           => array(
-				'version' => self::VERSION,
-				'spec'    => 'https://ucp.dev/' . self::VERSION . '/specification/cart',
-				'schema'  => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/cart.json',
-			),
-		);
-	}
-
-	/**
-	 * Spec and schema references for a capability.
-	 *
-	 * @param string $capability One of the CAPABILITY_* constants.
-	 * @return array
-	 */
-	private static function capability_declaration( string $capability ): array {
-		return self::capability_declarations()[ $capability ] ?? array( 'version' => self::VERSION );
-	}
-
-	/**
-	 * Build a UCP error response.
-	 *
-	 * The envelope shape every route shares. `$status` follows the UCP split: a malformed
-	 * request keeps a non-2xx status, a valid request that ends in a refusal is a business
-	 * outcome reported at 200 through `ucp.status` and `messages`.
-	 *
-	 * @param string $capability One of the CAPABILITY_* constants.
-	 * @param string $code Error code.
-	 * @param string $message Error message.
-	 * @param string $severity Error severity.
-	 * @param int    $status HTTP status.
-	 * @param string $path JSONPath of the offending field.
-	 * @return \WP_REST_Response
-	 */
-	public static function error_response( string $capability, string $code, string $message, string $severity, int $status, string $path = '$' ): \WP_REST_Response {
-		return new \WP_REST_Response(
-			array(
-				'ucp'      => self::response_metadata( $capability, 'error' ),
-				'messages' => array(
-					array(
-						'type'         => 'error',
-						'code'         => $code,
-						'content'      => $message,
-						'severity'     => $severity,
-						'path'         => $path,
-						'content_type' => 'plain',
-					),
-				),
-			),
-			$status
-		);
-	}
-
-	/**
-	 * Refuse a request that brought a body or form fields under anything other than a JSON content type.
-	 *
-	 * A UCP request is a JSON document, so a form or query payload is never read in its
-	 * place. A bare request, such as a `cancel`, carries nothing and is not refused.
-	 *
-	 * @param \WP_REST_Request $request Request object.
-	 * @param string           $capability One of the CAPABILITY_* constants.
-	 *
-	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
-	 *
-	 * @return \WP_REST_Response|null The 415 refusal, or null when the request may be read.
-	 */
-	public static function refuse_non_json_payload( \WP_REST_Request $request, string $capability ): ?\WP_REST_Response {
-		if ( $request->is_json_content_type() ) {
-			return null;
-		}
-
-		if ( '' === trim( (string) $request->get_body() ) && array() === $request->get_body_params() ) {
-			return null;
-		}
-
-		return self::error_response( $capability, 'VALIDATION_ERROR', __( 'UCP requests must be sent as application/json.', 'woocommerce' ), 'recoverable', 415 );
-	}
-
-	/**
-	 * Read a parameter from the JSON document alone.
-	 *
-	 * `get_param()` would also resolve it from the form body or the query string. UCP defines
-	 * the request as the document, so nothing carried elsewhere is allowed to act.
-	 *
-	 * @param \WP_REST_Request $request Request object.
-	 * @param string           $name Parameter name.
-	 *
-	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
-	 *
-	 * @return mixed|null The value, or null when the document has none.
-	 */
-	public static function json_param( \WP_REST_Request $request, string $name ) {
-		$params = $request->get_json_params();
-
-		return is_array( $params ) && array_key_exists( $name, $params ) ? $params[ $name ] : null;
-	}
-
-	/**
-	 * Translate a cart error code into the UCP code closest to it.
-	 *
-	 * The cart names more reasons than UCP does, so the ones an agent can act on
-	 * differently — retry smaller, fix the quantity, or re-read the cart — stay apart
-	 * and the rest collapse.
-	 *
-	 * @param string $code Store API error code.
-	 * @param string $fallback UCP code for a cart error UCP has no closer name for.
-	 * @return string One of `out_of_stock`, `not_found`, `invalid_quantity` or the fallback.
-	 */
-	public static function cart_error_code( string $code, string $fallback = 'item_unavailable' ): string {
-		switch ( $code ) {
-			case 'woocommerce_rest_product_out_of_stock':
-			case 'woocommerce_rest_product_partially_out_of_stock':
-			case 'woocommerce_rest_product_too_many_in_cart':
-				return 'out_of_stock';
-			case 'woocommerce_rest_cart_invalid_key':
-				return 'not_found';
-			case 'woocommerce_rest_product_invalid_quantity':
-			case 'invalid_quantity':
-			case 'readonly_quantity':
-				return 'invalid_quantity';
-			default:
-				return $fallback;
-		}
-	}
+class UcpUtils extends Utils {
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php b/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
index d9298eeb7ba..6ca0f2e2dc9 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
@@ -4,7 +4,7 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Tests\Internal\Ucp;

 use Automattic\WooCommerce\Internal\Ucp\UcpDiscovery;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;

 /**
  * Tests for the `/.well-known/ucp` discovery profile.
@@ -40,15 +40,15 @@ class UcpDiscoveryTest extends \WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox The profile points at the local UCP REST namespace and lists every capability UcpUtils declares.
+	 * @testdox The profile points at the local UCP REST namespace and lists every capability Utils declares.
 	 */
 	public function test_profile_shape(): void {
 		$profile = $this->sut->get_profile();

 		$this->assertSame( array( 'ucp', 'links' ), array_keys( $profile ) );
-		$this->assertSame( UcpUtils::VERSION, $profile['ucp']['version'] );
+		$this->assertSame( Utils::VERSION, $profile['ucp']['version'] );
 		$this->assertSame( rest_url( 'wc/ucp/v1' ), $profile['ucp']['services']['dev.ucp.shopping'][0]['endpoint'] );
-		$declarations = UcpUtils::capability_declarations();
+		$declarations = Utils::capability_declarations();
 		$this->assertSame( array_keys( $declarations ), array_keys( $profile['ucp']['capabilities'] ) );
 		foreach ( $declarations as $capability => $declaration ) {
 			$this->assertSame( array( $declaration ), $profile['ucp']['capabilities'][ $capability ] );
@@ -113,7 +113,7 @@ class UcpDiscoveryTest extends \WC_Unit_Test_Case {
 		$this->sut->maybe_flush_rewrite_rules( 'some_other_feature' );
 		$this->assertSame( 'no', get_option( 'woocommerce_queue_flush_rewrite_rules' ) );

-		$this->sut->maybe_flush_rewrite_rules( UcpUtils::FEATURE_ID );
+		$this->sut->maybe_flush_rewrite_rules( Utils::FEATURE_ID );
 		$this->assertSame( 'yes', get_option( 'woocommerce_queue_flush_rewrite_rules' ) );
 	}