# woocommerce-pos/1.10.1/includes/Sync/Pos_Uuid.php

WCPOS – Point of Sale (POS) plugin for WooCommerce, version 1.10.1. 657 lines.

- Page: https://pluginprobe.com/plugins/woocommerce-pos/1.10.1/code/includes/Sync/Pos_Uuid.php
- Raw: https://pluginprobe.com/plugins/woocommerce-pos/1.10.1/raw/includes/Sync/Pos_Uuid.php
- Modified: 2026-08-25T07:52:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/woocommerce-pos/1.10.1/code/includes/Sync/Pos_Uuid.php#L10-L20`.

```php
<?php
/**
 * WCPOS sync read surface.
 *
 * @package WCPOS\WooCommercePOS\Sync
 */

namespace WCPOS\WooCommercePOS\Sync;

use Exception;
use Ramsey\Uuid\Uuid;
use WC_Customer;
use WC_Order_Item;
use WCPOS\WooCommercePOS\Logger;

// phpcs:disable Squiz.Commenting, Generic.Commenting -- Ported lab documentation is preserved verbatim.
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Queries are prepared before execution.

/**
 * Uniform record identity on the server side — the sole authority that stamps,
 * validates, deduplicates, and checks ownership of `_woocommerce_pos_uuid`.
 * Legacy API callers delegate here (ADR 0021, decision c).
 *
 * The client uses this uuid as the stable RxDB primary key (ADR 0008, guardrail
 * G1): a record carries the SAME identity on the server and the client, may be
 * born on either side, and is NEVER re-keyed. This is the server half of that
 * contract — a record pulled from the lab namespace arrives WITH its uuid, so the
 * client never has to mint a divergent one for a server-born record. Multisite
 * customer first-stamps are serialized while adopting legacy per-blog values.
 *
 * Reads an existing valid uuid from the record's meta; if absent/invalid it
 * generates one and PERSISTS it (so it is stable across pulls), then mirrors it
 * into the serialized payload's `meta_data`. Duck-typed on the WC_Data methods so
 * it stays unit-testable without WooCommerce loaded. UUID convergence does not
 * use an object-cache lock; sync writes are serialized by their record lock,
 * while stamping deterministically converges duplicate meta rows.
 */
class Pos_Uuid {
	public const META_KEY = Api::UUID_META_KEY;
	/** Meta key carrying the freshly-recomputed variable-product price range (P2-2). */

	/**
	 * A standard 8-4-4-4-12 uuid shape (any version), case-insensitive.
	 */
	public static function is_uuid( $value ): bool {
		return \is_string( $value )
			 && (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value );
	}

	/**
	 * The first VALID uuid among a record's meta entries (WC_Meta_Data objects
	 * with ->key/->value, or arrays), or '' if none. Skips blank / invalid / a
	 * blank duplicate in favour of a later valid one.
	 */
	public static function read_valid_uuid_from_meta( array $meta_data ): string {
		foreach ( $meta_data as $meta ) {
			$key = Meta_Entry::key( $meta );
			if ( self::META_KEY !== $key ) {
				continue;
			}
			$value = Meta_Entry::value( $meta );
			if ( self::is_uuid( $value ) ) {
				return $value;
			}
		}

		return '';
	}

	/**
	 * Ensure the record carries a stable, UNIQUELY-OWNED uuid: reuse a valid
	 * existing one, else generate + persist a new one. Returns the uuid (or '' if
	 * the object can't carry meta). Duck-typed on get_meta_data / update_meta_data
	 * / save_meta_data.
	 *
	 * $opts['collides'] is an optional callable (uuid, object) => bool: when it
	 * reports the existing uuid is already owned by ANOTHER record (a clone/import
	 * that copied the meta), we treat it as needing a fresh one rather than serving
	 * a duplicate RxDB key. Injected so the branching stays unit-testable; the live
	 * wiring uses the $wpdb-backed self::uuid_owned_by_other.
	 *
	 * @param mixed $object
	 */
	public static function ensure_uuid( $object, array $opts = array() ): string {
		if ( ! \is_object( $object ) || ! method_exists( $object, 'get_meta_data' ) ) {
			return '';
		}
		if (
			$object instanceof WC_Customer
			&& \function_exists( 'is_multisite' )
			&& is_multisite()
			&& '' === self::read_valid_uuid_from_meta( (array) $object->get_meta_data() )
		) {
			return self::ensure_multisite_customer_uuid( $object, $opts );
		}

		return self::ensure_uuid_without_user_lock( $object, $opts );
	}

	/**
	 * Ensure a UUID after any customer-specific coordination has completed.
	 *
	 * @param mixed $object
	 */
	private static function ensure_uuid_without_user_lock( $object, array $opts ): string {
		$collides = $opts['collides'] ?? null;
		$persist  = $opts['persist'] ?? true;
		$existing = self::read_valid_uuid_from_meta( (array) $object->get_meta_data() );
		if ( '' !== $existing && ! ( \is_callable( $collides ) && $collides( $existing, $object ) ) ) {
			// Converge any duplicate uuid metas (e.g. a concurrent first-stamp) to
			// the single canonical value — deterministic regardless of object-cache
			// backend, so no cross-request lock is required for correctness.
			self::prune_duplicate_uuid_meta( $object, $persist );

			return $existing;
		}
		if ( ! method_exists( $object, 'update_meta_data' ) ) {
			return '';
		}
		// Minting persists by default: a freshly-generated uuid that isn't written
		// back would differ on the next pull, making identity unstable — worse than
		// none. persist:false is for a BEFORE-save hook, where the in-progress save
		// writes the meta, so we add it but skip a redundant second save.
		if ( $persist && ! method_exists( $object, 'save_meta_data' ) ) {
			return '';
		}
		$uuid = self::generate_uuid();
		$object->update_meta_data( self::META_KEY, $uuid );
		if ( $persist ) {
			call_user_func( array( $object, 'save_meta_data' ) );
			// A concurrent first-stamp may have persisted its own uuid between our
			// read and save. Re-read and converge on the first-valid row so every
			// racer returns the SAME winner instead of each serving its own mint.
			if ( $object instanceof \WC_Data ) {
				$object->read_meta_data( true );
				self::prune_duplicate_uuid_meta( $object, true );
				$stored = self::read_valid_uuid_from_meta( (array) $object->get_meta_data() );
				if ( self::is_uuid( $stored ) ) {
					return $stored;
				}
			}
		}

		return $uuid;
	}

	/**
	 * Legacy WP_User adapter for the shared WC_Data identity path (ADR 0021).
	 *
	 * @param mixed $user WP_User-like object or numeric user id.
	 */
	public static function ensure_user_uuid( $user ): string {
		$user_id = \is_object( $user ) && isset( $user->ID ) ? (int) $user->ID : (int) $user;
		if ( $user_id <= 0 || ! class_exists( WC_Customer::class ) ) {
			return '';
		}

		try {
			$customer = new WC_Customer( $user_id );
		} catch ( Exception $e ) {
			Logger::log( 'Unable to load customer for UUID stamping: ' . $e->getMessage() );
			return '';
		}

		if ( ! method_exists( $customer, 'get_id' ) || $user_id !== (int) $customer->get_id() ) {
			return '';
		}

		return self::ensure_uuid(
			$customer,
			array( 'collides' => array( __CLASS__, 'uuid_owned_by_other_user' ) )
		);
	}

	/**
	 * Ensure the WC_Order_Item has a valid UUID.
	 *
	 * @param WC_Order_Item $item The order item object.
	 * @return void
	 */
	public static function ensure_order_item_uuid( WC_Order_Item $item ): void {
		global $wpdb;

		if ( self::is_uuid( $item->get_meta( self::META_KEY ) ) ) {
			return;
		}

		$lock_key = 'wc_pos_uuid_order_item_' . $item->get_id();
		$acquired = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $lock_key, 10 ) );
		if ( '1' !== (string) $acquired ) {
			Logger::log( 'Unable to acquire lock for order item UUID update for order item id ' . $item->get_id() );
			return;
		}
		try {
			// Persist any pending meta, then check the STORED uuid directly —
			// a full read_meta_data(true) reload would clobber sibling in-memory
			// meta on lanes where the datastore cache lags (HPOS misc `_sku`).
			$item->save_meta_data();
			$uuid = wc_get_order_item_meta( $item->get_id(), self::META_KEY, true );
			if ( ! self::is_uuid( $uuid ) ) {
				$uuid = Uuid::uuid4()->toString();
				$item->update_meta_data( self::META_KEY, $uuid );
				$item->save_meta_data();
			} elseif ( $uuid !== $item->get_meta( self::META_KEY ) ) {
				// A concurrent request minted first; converge the stale in-memory
				// item on the stored winner so the served payload carries it.
				$item->update_meta_data( self::META_KEY, $uuid );
			}
		} finally {
			$wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_key ) );
		}
	}

	/**
	 * Promote a legacy per-blog cashier uuid to the network-wide key.
	 *
	 * Before identity consolidated here, the cashier endpoint minted
	 * `_woocommerce_pos_uuid_{blog_id}` on multisite while every other reader used
	 * the plain network-wide key — forking one user into two RxDB identities. When
	 * the plain key holds no valid uuid yet, adopt the current blog's legacy value
	 * so existing multisite cashiers keep their identity regardless of which
	 * endpoint reads them first. An existing valid plain uuid wins, because it is
	 * what /customers has already served to clients. Legacy rows are left in place
	 * (harmless, rollback-safe). A legacy value owned by ANOTHER user — under the
	 * network key or the current blog's legacy key — is never adopted: the first
	 * reader must not claim a shared legacy uuid as their network identity
	 * (#1465). When two users share the same legacy value, neither adopts it and
	 * both are minted fresh; the 1.10.0 migration resolves the ambiguity for
	 * users it reaches first.
	 *
	 * @param WC_Customer $customer Customer being stamped.
	 */
	private static function adopt_legacy_multisite_user_uuid( WC_Customer $customer ): void {
		$existing = self::read_valid_uuid_from_meta( (array) $customer->get_meta_data() );
		if ( self::is_uuid( $existing ) ) {
			return;
		}

		$user_id = (int) $customer->get_id();
		$legacy = get_user_meta( $user_id, self::META_KEY . '_' . get_current_blog_id(), true );
		if ( self::is_uuid( $legacy ) && ! self::legacy_uuid_owned_by_other_user( $legacy, $customer ) ) {
			// Never clobber a uuid another request persisted concurrently — its
			// client is already keyed on it. A unique add when no row exists; a
			// compare-and-swap against the OBSERVED invalid value otherwise, so a
			// lock-timeout fallback that replaced the corrupt row between our
			// read and this write survives (the CAS no-ops and ensure_uuid then
			// serves the concurrent winner).
			$stored_rows = get_user_meta( $user_id, self::META_KEY, false );
			if ( array() === $stored_rows ) {
				add_user_meta( $user_id, self::META_KEY, $legacy, true );
			} elseif ( ! self::is_uuid( $stored_rows[0] ) ) {
				update_user_meta( $user_id, self::META_KEY, $legacy, $stored_rows[0] );
			}
		}
	}

	/**
	 * Serialize first-stamp and legacy adoption for one multisite customer.
	 */
	private static function ensure_multisite_customer_uuid( WC_Customer $customer, array $opts ): string {
		global $wpdb;

		$user_id   = (int) $customer->get_id();
		$lock_name = 'wcpos_user_uuid_' . $user_id;
		$acquired  = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $lock_name, 5 ) );
		if ( '1' !== (string) $acquired ) {
			// The lock holder is (or was) stamping this user. This uuid is the
			// client's RxDB primary key, so never serve '' — and when a legacy
			// identity may be mid-adoption, serve it read-only rather than writing
			// anything that would pre-empt the adoption and fork the user.
			$customer->read_meta_data( true );
			$persisted = self::read_valid_uuid_from_meta( (array) $customer->get_meta_data() );
			if ( self::is_uuid( $persisted ) ) {
				return $persisted;
			}

			// An adoptable legacy uuid is what the holder will promote — serve the
			// same value read-only so this response and the adoption agree.
			$legacy = get_user_meta( $user_id, self::META_KEY . '_' . get_current_blog_id(), true );
			if ( self::is_uuid( $legacy ) && ! self::legacy_uuid_owned_by_other_user( $legacy, $customer ) ) {
				return $legacy;
			}

			// First stamp under contention: persist a fallback without clobbering a
			// concurrent winner — a unique add when no row exists, a compare-and-swap
			// against the (invalid) first row otherwise — then serve whichever row
			// stuck so every racer converges on one persisted identity.
			$fallback    = self::generate_uuid();
			$stored_rows = get_user_meta( $user_id, self::META_KEY, false );
			if ( array() === $stored_rows ) {
				add_user_meta( $user_id, self::META_KEY, $fallback, true );
			} elseif ( ! self::is_uuid( $stored_rows[0] ) ) {
				update_user_meta( $user_id, self::META_KEY, $fallback, $stored_rows[0] );
			}
			$stored = get_user_meta( $user_id, self::META_KEY, true );

			return self::is_uuid( $stored ) ? $stored : $fallback;
		}

		try {
			$customer->read_meta_data( true );
			self::adopt_legacy_multisite_user_uuid( $customer );
			$customer->read_meta_data( true );

			return self::ensure_uuid_without_user_lock( $customer, $opts );
		} finally {
			$wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) );
		}
	}

	/**
	 * Legacy WP_Term adapter for the shared identity path (ADR 0021).
	 *
	 * @param mixed $term WP_Term-like object or numeric term id.
	 */
	public static function ensure_term_uuid( $term ): string {
		$term_id = \is_object( $term ) && isset( $term->term_id ) ? (int) $term->term_id : (int) $term;
		if ( $term_id <= 0 ) {
			return '';
		}

		$adapter = new Term_Meta_Adapter( $term_id );

		return self::ensure_uuid(
			$adapter,
			array( 'collides' => array( __CLASS__, 'uuid_owned_by_other_term' ) )
		);
	}

	/**
	 * Return a copy of the SERIALIZED payload whose `meta_data` mirrors `$uuid`
	 * exactly once (entries here are arrays: ['id'=>,'key'=>,'value'=>]). Drops
	 * blank / duplicate / mismatched `_woocommerce_pos_uuid` entries so the served
	 * record always carries one canonical identity.
	 */
	public static function ensure_in_payload( array $payload, string $uuid ): array {
		$meta   = ( isset( $payload['meta_data'] ) && \is_array( $payload['meta_data'] ) ) ? $payload['meta_data'] : array();
		$others = array();
		foreach ( $meta as $entry ) {
			$key = Meta_Entry::key( $entry );
			if ( self::META_KEY !== $key ) {
				$others[] = $entry;
			}
		}
		$others[]             = array(
			'key' => self::META_KEY,
			'value' => $uuid,
		);
		$payload['meta_data'] = array_values( $others );

		return $payload;
	}

	/**
	 * Hook for the per-collection `woocommerce_pos_sync_serialized_*` filters (product,
	 * order, …): stamp the served record's stable uuid (persisting a new one if
	 * needed) and mirror it into the payload, so EVERY read carries the identity the
	 * client keys on — regardless of how the record was born. A non-array payload or
	 * an object that can't carry meta passes through unchanged.
	 *
	 * Collection-agnostic: `ensure_uuid` only needs the WC_Data meta API
	 * (get/update/save_meta_data), which orders (HPOS-safe), customers, and terms all
	 * provide. Collision detection IS storage-specific: orders live in HPOS tables
	 * (not `wp_postmeta`), so they get the order-aware detector; products/variations
	 * keep the post-scoped one. Customers and terms use their storage-specific
	 * adapters and detectors.
	 *
	 * @param mixed      $payload
	 * @param mixed      $object
	 * @param null|mixed $request
	 */
	public static function stamp_serialized_record( $payload, $object, $request = null ) {
		if ( ! \is_array( $payload ) ) {
			return $payload;
		}
		$collides = is_a( $object, 'WC_Abstract_Order' )
			? array( __CLASS__, 'uuid_owned_by_other_order' )
			: array( __CLASS__, 'uuid_owned_by_other' );
		$uuid = self::ensure_uuid( $object, array( 'collides' => $collides ) );

		return '' === $uuid ? $payload : self::ensure_in_payload( $payload, $uuid );
	}

	/**
	 * Order-aware variant of {@see uuid_owned_by_other}. HPOS order meta does NOT live
	 * in `wp_postmeta`, so the post-scoped detector can't see an order that already owns
	 * `$uuid` — a duplicated/imported order with a copied uuid would slip through and two
	 * orders would share one RxDB key. Query the orders store (HPOS-safe via
	 * `wc_get_orders`) for the uuid; a match on a DIFFERENT order id is a real collision.
	 *
	 * @param mixed $uuid
	 * @param mixed $object
	 */
	public static function uuid_owned_by_other_order( $uuid, $object ): bool {
		if ( ! \is_object( $object ) || ! method_exists( $object, 'get_id' ) || ! \function_exists( 'wc_get_orders' ) ) {
			return false;
		}
		$order_id = (int) $object->get_id();
		foreach ( self::get_order_ids_by_uuid( (string) $uuid ) as $other_id ) {
			if ( (int) $other_id !== $order_id ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Return at most two order ids carrying a UUID. The legacy create controller
	 * treats two results as an ambiguous identity and fails closed.
	 *
	 * Datastore-aware direct meta lookup: under HPOS the uuid lives in
	 * `wc_orders_meta`, otherwise in `wp_postmeta`. `wc_get_orders()` with a
	 * `meta_query` is NOT supported on the CPT order datastore (it fires a
	 * `doing_it_wrong` and returns unfiltered results), so we query the meta table
	 * directly — the same shape the plugin's other order-uuid lookups use.
	 */
	public static function get_order_ids_by_uuid( string $uuid ): array {
		global $wpdb;
		if ( ! isset( $wpdb ) ) {
			return array();
		}

		$order_util = '\\Automattic\\WooCommerce\\Utilities\\OrderUtil';
		$hpos       = class_exists( $order_util )
			&& method_exists( $order_util, 'custom_orders_table_usage_is_enabled' )
			&& call_user_func( array( $order_util, 'custom_orders_table_usage_is_enabled' ) );

		if ( $hpos ) {
			$ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT DISTINCT m.order_id FROM {$wpdb->prefix}wc_orders_meta m"
					. " JOIN {$wpdb->prefix}wc_orders o ON o.id = m.order_id AND o.type = 'shop_order'"
					. ' WHERE m.meta_key = %s AND m.meta_value = %s'
					. " AND o.status NOT IN ('trash','auto-draft')"
					. ' ORDER BY m.order_id ASC LIMIT 2',
					self::META_KEY,
					$uuid
				)
			);
		} else {
			$ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT DISTINCT m.post_id FROM {$wpdb->postmeta} m"
					. " JOIN {$wpdb->posts} p ON p.ID = m.post_id AND p.post_type = 'shop_order'"
					. ' WHERE m.meta_key = %s AND m.meta_value = %s'
					. " AND p.post_status NOT IN ('trash','auto-draft')"
					. ' ORDER BY m.post_id ASC LIMIT 2',
					self::META_KEY,
					$uuid
				)
			);
		}

		return \is_array( $ids ) ? array_values( $ids ) : array();
	}

	/**
	 * Register WRITE-time stamping: a record gets its uuid the moment it is saved,
	 * so every read path (catalog proxy, change-signal hydration) then serves it
	 * straight from postmeta with no per-path stamping copy. Hooked BEFORE the data
	 * store writes, so the uuid lands in the SAME save — no second write, no
	 * change-log cascade, and no concurrent-first-READ race (stamping is per-save,
	 * not per-reader). The read-time filter remains as a fallback for records that
	 * predate these hooks until the backfill runs.
	 */
	public static function register_hooks(): void {
		if ( ! \function_exists( 'add_action' ) ) {
			return;
		}
		add_action( 'woocommerce_before_product_object_save', array( __CLASS__, 'stamp_on_save' ), 10, 1 );
		add_action( 'woocommerce_before_product_variation_object_save', array( __CLASS__, 'stamp_on_save' ), 10, 1 );
	}

	/**
	 * Before-save hook: ensure the WC object carries a unique uuid as part of the
	 * in-progress save (persist:false — the save itself writes it).
	 *
	 * @param mixed $object
	 */
	public static function stamp_on_save( $object ): void {
		self::ensure_uuid(
			$object,
			array(
				'collides' => array( __CLASS__, 'uuid_owned_by_other' ),
				'persist'  => false,
			)
		);
	}

	/**
	 * True when $uuid is already stored as `_woocommerce_pos_uuid` on a DIFFERENT
	 * post (a cloned/imported record that copied the meta). Post-scoped (products
	 * + variations); terms/customers live in their own meta tables and get their
	 * own detector when those seams land. Returns false when $wpdb or the object's
	 * id is unavailable (e.g. unit tests inject a fake detector instead).
	 *
	 * @param mixed $uuid
	 * @param mixed $object
	 */
	public static function uuid_owned_by_other( $uuid, $object ): bool {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! \is_object( $object ) || ! method_exists( $object, 'get_id' ) ) {
			return false;
		}
		// Only an ACTIVE post counts as a live owner — a trashed/auto-draft record
		// sharing the uuid is not a real collision (it will never be served), so it
		// must not force a needless regeneration on the active record.
		$sql = $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->postmeta} m
             JOIN {$wpdb->posts} p ON p.ID = m.post_id
             WHERE m.meta_key = %s AND m.meta_value = %s AND m.post_id <> %d
               AND p.post_status NOT IN ('trash','auto-draft')",
			self::META_KEY,
			$uuid,
			(int) $object->get_id()
		);

		return (int) $wpdb->get_var( $sql ) > 0;
	}

	/**
	 * User-table twin of uuid_owned_by_other for CUSTOMERS (WC_Customer over
	 * wp_usermeta). DELIBERATE asymmetry: WP users have no post_status / trash, so
	 * every user row carrying the uuid is a live owner — there is NO status
	 * exclusion (a status filter here would reference a non-existent column).
	 *
	 * @param mixed $uuid
	 * @param mixed $object
	 */
	public static function uuid_owned_by_other_user( $uuid, $object ): bool {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! \is_object( $object ) || ! method_exists( $object, 'get_id' ) ) {
			return false;
		}
		$sql = $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->usermeta} m
             JOIN {$wpdb->users} u ON u.ID = m.user_id
             WHERE m.meta_key = %s AND m.meta_value = %s AND m.user_id <> %d",
			self::META_KEY,
			$uuid,
			(int) $object->get_id()
		);

		return (int) $wpdb->get_var( $sql ) > 0;
	}

	/**
	 * Adoption-gate twin of uuid_owned_by_other_user(): also checks the CURRENT
	 * blog's legacy per-blog key (`_woocommerce_pos_uuid_{blog}`), because a
	 * value another user still holds under that legacy key is the same RxDB
	 * primary key, and adopting it would fork that user's identity (#1465).
	 *
	 * Deliberately NOT used for the live-identity collision check: a stale
	 * duplicated legacy row must never discard an already-served network uuid,
	 * so `collides` stays scoped to the network key.
	 *
	 * @param mixed       $uuid     Candidate legacy uuid.
	 * @param WC_Customer $customer Customer being stamped.
	 */
	private static function legacy_uuid_owned_by_other_user( $uuid, WC_Customer $customer ): bool {
		global $wpdb;
		if ( self::uuid_owned_by_other_user( $uuid, $customer ) ) {
			return true;
		}
		if ( ! isset( $wpdb ) ) {
			return false;
		}
		$sql = $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->usermeta} m
             JOIN {$wpdb->users} u ON u.ID = m.user_id
             WHERE m.meta_key = %s AND m.meta_value = %s AND m.user_id <> %d",
			self::META_KEY . '_' . get_current_blog_id(),
			$uuid,
			(int) $customer->get_id()
		);

		return (int) $wpdb->get_var( $sql ) > 0;
	}

	/**
	 * Term-table twin for CATEGORIES + BRANDS. CROSS-TAXONOMY by design: product_cat
	 * and product_brand SHARE wp_termmeta, so a uuid on a different term in EITHER
	 * taxonomy is a real RxDB-primary-key clash — match on term_id across ALL
	 * taxonomies (NO taxonomy scoping). Terms have no trash/status, so no status
	 * exclusion (like users, unlike posts).
	 *
	 * @param mixed $uuid
	 * @param mixed $object
	 */
	public static function uuid_owned_by_other_term( $uuid, $object ): bool {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! \is_object( $object ) || ! method_exists( $object, 'get_id' ) ) {
			return false;
		}
		$sql = $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->termmeta} WHERE meta_key = %s AND meta_value = %s AND term_id <> %d",
			self::META_KEY,
			$uuid,
			(int) $object->get_id()
		);

		return (int) $wpdb->get_var( $sql ) > 0;
	}

	/**
	 * A v4 uuid — `wp_generate_uuid4()` under WordPress, else a local fallback.
	 */
	public static function generate_uuid(): string {
		if ( \function_exists( 'wp_generate_uuid4' ) ) {
			return wp_generate_uuid4();
		}
		$data    = random_bytes( 16 );
		$data[6] = \chr( ( \ord( $data[6] ) & 0x0f ) | 0x40 ); // version 4
		$data[8] = \chr( ( \ord( $data[8] ) & 0x3f ) | 0x80 ); // variant 10xx

		return vsprintf( '%s%s-%s-%s-%s-%s%s%s', str_split( bin2hex( $data ), 4 ) );
	}

	/**
	 * Collapse duplicate `_woocommerce_pos_uuid` metas to ONE canonical entry —
	 * keep the first valid uuid, delete every other uuid meta (a concurrent-stamp
	 * duplicate, or a blank/invalid straggler). Only persisted metas (with a meta
	 * id) are deleted; a freshly-added unsaved one is left for the in-progress save.
	 * When $persist, the cleanup is saved immediately; otherwise the ongoing save
	 * (before-save hook) applies the deletions. Mirrors production's dedup, and is
	 * what makes the concurrent-first-stamp outcome correct without a lock.
	 *
	 * @param mixed $object
	 */
	private static function prune_duplicate_uuid_meta( $object, bool $persist ): void {
		if ( ! \is_object( $object ) || ! method_exists( $object, 'get_meta_data' ) || ! method_exists( $object, 'delete_meta_data_by_mid' ) ) {
			return;
		}
		$kept_valid = false;
		$deleted    = false;
		foreach ( (array) $object->get_meta_data() as $meta ) {
			if ( ! \is_object( $meta ) || self::META_KEY !== Meta_Entry::key( $meta ) ) {
				continue;
			}
			if ( ! $kept_valid && self::is_uuid( Meta_Entry::value( $meta ) ) ) {
				$kept_valid = true; // keep the first valid uuid meta

				continue;
			}
			$mid = $meta->id ?? null;
			if ( null !== $mid ) {
				$object->delete_meta_data_by_mid( $mid );
				$deleted = true;
			}
		}
		if ( $deleted && $persist && method_exists( $object, 'save_meta_data' ) ) {
			$object->save_meta_data();
		}
	}
}

```
