Commit f0884634f73 for woocommerce
commit f0884634f7334f5e51c72d5b2cb40a8be4bec29e
Author: AnnaMag <AnnaMag@users.noreply.github.com>
Date: Mon Nov 2 23:44:23 2026 +0100
Send each UCP catalog variant's GTIN as a barcode (#70351)
* Send each UCP catalog variant's GTIN as a barcode
UCP variants carry product identifiers in barcodes, but the mapper sent
none, so agents could not match a product by GTIN even when the store set
one. Normalize and validate the GTIN the way the product structured data
does, and send a variation's own GTIN only.
* Cast the normalized GTIN to string for PHPStan
* Validate UCP barcodes with the structured data GTIN helpers
So the catalog and the product structured data cannot disagree on a GTIN.
diff --git a/plugins/woocommerce/changelog/add-ucp-variant-barcodes b/plugins/woocommerce/changelog/add-ucp-variant-barcodes
new file mode 100644
index 00000000000..9d8a8135e7a
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-ucp-variant-barcodes
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+UCP catalog: send each variant's GTIN as a barcode.
diff --git a/plugins/woocommerce/src/Internal/ProductFeed/Mapping/UcpProductMapper.php b/plugins/woocommerce/src/Internal/ProductFeed/Mapping/UcpProductMapper.php
index 4686fb24d64..a930044991b 100644
--- a/plugins/woocommerce/src/Internal/ProductFeed/Mapping/UcpProductMapper.php
+++ b/plugins/woocommerce/src/Internal/ProductFeed/Mapping/UcpProductMapper.php
@@ -501,7 +501,7 @@ class UcpProductMapper implements ProductShapeMapperInterface {
*/
private function build_variants( \WC_Product $product, array $item, string $currency, array $required_variation_ids, bool $only_required, ?array $candidates ): array {
if ( ! $product instanceof \WC_Product_Variable ) {
- return array( $this->build_simple_variant( $item, $currency ) );
+ return array( $this->build_simple_variant( $product, $item, $currency ) );
}
$children = array_map( 'intval', null === $candidates ? $product->get_visible_children() : $candidates );
@@ -557,7 +557,7 @@ class UcpProductMapper implements ProductShapeMapperInterface {
continue;
}
- $variants[] = $this->build_variation_variant( $this->item( $variation ), $options[ (int) $variation_id ] ?? array(), $currency );
+ $variants[] = $this->build_variation_variant( $variation, $this->item( $variation ), $options[ (int) $variation_id ] ?? array(), $currency );
}
return $variants;
@@ -585,11 +585,12 @@ class UcpProductMapper implements ProductShapeMapperInterface {
/**
* Build a variant entry from a simple product's item response.
*
- * @param array $item Store API item response.
- * @param string $currency ISO 4217 currency code.
+ * @param \WC_Product $product The product.
+ * @param array $item Store API item response.
+ * @param string $currency ISO 4217 currency code.
* @return array
*/
- private function build_simple_variant( array $item, string $currency ): array {
+ private function build_simple_variant( \WC_Product $product, array $item, string $currency ): array {
$variant = array(
'id' => (string) $item['id'],
'title' => (string) $item['name'],
@@ -603,6 +604,11 @@ class UcpProductMapper implements ProductShapeMapperInterface {
$variant['sku'] = $sku;
}
+ $barcodes = $this->build_barcodes( $product );
+ if ( ! empty( $barcodes ) ) {
+ $variant['barcodes'] = $barcodes;
+ }
+
$list_price = $this->build_variant_list_price( $item['prices'], $currency );
if ( null !== $list_price ) {
$variant['list_price'] = $list_price;
@@ -614,12 +620,13 @@ class UcpProductMapper implements ProductShapeMapperInterface {
/**
* Build a variant entry from a variation's item response.
*
- * @param array $item Store API item response of the variation.
- * @param array $options Selected options, each `{ name, label }`.
- * @param string $currency ISO 4217 currency code.
+ * @param \WC_Product_Variation $variation The variation.
+ * @param array $item Store API item response of the variation.
+ * @param array $options Selected options, each `{ name, label }`.
+ * @param string $currency ISO 4217 currency code.
* @return array
*/
- private function build_variation_variant( array $item, array $options, string $currency ): array {
+ private function build_variation_variant( \WC_Product_Variation $variation, array $item, array $options, string $currency ): array {
$labels = array_column( $options, 'label' );
$variant = array(
@@ -635,6 +642,11 @@ class UcpProductMapper implements ProductShapeMapperInterface {
$variant['sku'] = $sku;
}
+ $barcodes = $this->build_barcodes( $variation );
+ if ( ! empty( $barcodes ) ) {
+ $variant['barcodes'] = $barcodes;
+ }
+
$list_price = $this->build_variant_list_price( $item['prices'], $currency );
if ( null !== $list_price ) {
$variant['list_price'] = $list_price;
@@ -652,6 +664,33 @@ class UcpProductMapper implements ProductShapeMapperInterface {
return $variant;
}
+ /**
+ * Build a variant's barcodes from its GTIN, normalized and validated by the product structured data,
+ * so the catalog and the structured data never disagree on a GTIN.
+ *
+ * A variation's GTIN is its own: the parent's identifies a different item.
+ *
+ * @param \WC_Product $product The product or variation.
+ * @return array Empty, or one `{ type, value }` barcode.
+ */
+ private function build_barcodes( \WC_Product $product ): array {
+ $structured_data = WC()->structured_data;
+ if ( ! $structured_data instanceof \WC_Structured_Data ) {
+ return array();
+ }
+ $gtin = (string) $structured_data->prepare_gtin( (string) $product->get_global_unique_id() );
+ if ( ! $structured_data->is_valid_gtin( $gtin ) ) {
+ return array();
+ }
+
+ return array(
+ array(
+ 'type' => 'GTIN',
+ 'value' => $gtin,
+ ),
+ );
+ }
+
/**
* Build a variant's list price when it is genuinely discounted.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Mapping/UcpProductMapperTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Mapping/UcpProductMapperTest.php
index 9c64fce1939..6306d2e30d5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Mapping/UcpProductMapperTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Mapping/UcpProductMapperTest.php
@@ -322,4 +322,52 @@ class UcpProductMapperTest extends \WC_Unit_Test_Case {
$mapped['variants'][0]['options']
);
}
+
+ /**
+ * Agents match offers across stores by GTIN, so a valid one must reach the
+ * variant, normalized to digits, and an invalid one must not.
+ *
+ * @testdox A valid GTIN should be sent as a variant barcode and an invalid one should be left out.
+ */
+ public function test_valid_gtin_is_sent_as_a_variant_barcode(): void {
+ $mapper = new UcpProductMapper( 'USD' );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_global_unique_id( '4006-3813-3393-1' );
+ $product->save();
+
+ $this->assertSame(
+ array(
+ array(
+ 'type' => 'GTIN',
+ 'value' => '4006381333931',
+ ),
+ ),
+ $mapper->map_product( $product )['variants'][0]['barcodes']
+ );
+
+ $product->set_global_unique_id( '12345' );
+ $product->save();
+
+ $this->assertArrayNotHasKey( 'barcodes', $mapper->map_product( $product )['variants'][0] );
+ }
+
+ /**
+ * @testdox Each variation should send its own GTIN, not the parent's.
+ */
+ public function test_variation_sends_its_own_gtin(): void {
+ $variable = WC_Helper_Product::create_variation_product();
+ $variable->set_global_unique_id( '96385074' );
+ $variable->save();
+
+ $children = $variable->get_children();
+ $own = wc_get_product( $children[0] );
+ $own->set_global_unique_id( '4006381333931' );
+ $own->save();
+
+ $variants = array_column( ( new UcpProductMapper( 'USD' ) )->map_product( wc_get_product( $variable->get_id() ) )['variants'], null, 'id' );
+
+ $this->assertSame( '4006381333931', $variants[ (string) $children[0] ]['barcodes'][0]['value'] );
+ $this->assertArrayNotHasKey( 'barcodes', $variants[ (string) $children[1] ] );
+ }
}