Commit 39be92243ab for woocommerce
commit 39be92243ab0167ded779e08bcb9a78b2190c04b
Author: Ján Mikláš <neosinner@gmail.com>
Date: Tue May 19 12:55:37 2026 +0200
Add offer-level product price currency (#65123)
* Add offer-level structured data price currency
* Cover sale-price case for offer-level priceCurrency
Add a structured-data test for the on-sale path so the offer-level
`price` is locked to the sale price (matching what `priceSpecification[0]`
records). Add a changelog fragment.
* Fix structured data PHPStan warning
* Fix structured data PHPStan guard
diff --git a/plugins/woocommerce/changelog/wooplug-5442-product-offer-price-currency b/plugins/woocommerce/changelog/wooplug-5442-product-offer-price-currency
new file mode 100644
index 00000000000..d38f11b9fa0
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-5442-product-offer-price-currency
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Promote the simple-product offer's price and currency to the top of the JSON-LD offer so Google Search Console accepts the structured data.
diff --git a/plugins/woocommerce/includes/class-wc-structured-data.php b/plugins/woocommerce/includes/class-wc-structured-data.php
index 316504715b0..d0318c43b58 100644
--- a/plugins/woocommerce/includes/class-wc-structured-data.php
+++ b/plugins/woocommerce/includes/class-wc-structured-data.php
@@ -418,11 +418,14 @@ class WC_Structured_Data {
'url' => $shop_url,
),
);
+ if ( 'Offer' === $markup_offer['@type'] && empty( $markup_offer['price'] ) && ! empty( $markup_offer['priceSpecification'][0]['price'] ) ) {
+ $markup_offer['price'] = $markup_offer['priceSpecification'][0]['price'];
+ }
if (
( ! empty( $markup_offer['price'] ) ||
! empty( $markup_offer['lowPrice'] ) ||
! empty( $markup_offer['highPrice'] )
- ) && empty( $markup_offer['priceCurrency'] )
+ )
) {
$markup_offer['priceCurrency'] = $currency;
}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 0432d84946c..cd79245e2b4 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -15660,12 +15660,6 @@ parameters:
count: 1
path: includes/class-wc-structured-data.php
- -
- message: '#^Offset ''priceCurrency'' on array\{priceValidUntil\: string, availability\: ''https\://schema\.org…'', url\: mixed, seller\: array\{''@type''\: ''Organization'', name\: mixed, url\: mixed\}, ''@type''\: ''AggregateOffer'', lowPrice\: mixed, highPrice\: mixed, offerCount\: int\<0, max\>, \.\.\.\} in empty\(\) does not exist\.$#'
- identifier: empty.offset
- count: 1
- path: includes/class-wc-structured-data.php
-
-
message: '#^Parameter \#1 \$attachment_id of function wp_get_attachment_url expects int, string given\.$#'
identifier: argument.type
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php b/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php
index 652d1199e92..a0a25c41367 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php
@@ -80,4 +80,48 @@ class WC_Structured_Data_Test extends \WC_Unit_Test_Case {
$this->assertEquals( $this->structured_data->prepare_gtin( '+12345678' ), '12345678' );
$this->assertEquals( $this->structured_data->prepare_gtin( '123.4e-5' ), '12345' );
}
+
+ /**
+ * Test simple product offer structured data includes offer-level price currency.
+ *
+ * @return void
+ */
+ public function test_simple_product_offer_includes_offer_level_price_currency(): void {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_regular_price( '97' );
+ $product->set_price( '97' );
+ $product->save();
+
+ $this->structured_data->generate_product_data( $product );
+
+ $data = $this->structured_data->get_data();
+ $offer = $data[0]['offers'][0];
+
+ $this->assertEquals( '97.00', $offer['price'] );
+ $this->assertEquals( get_woocommerce_currency(), $offer['priceCurrency'] );
+ $this->assertEquals( get_woocommerce_currency(), $offer['priceSpecification'][0]['priceCurrency'] );
+ }
+
+ /**
+ * Test on-sale simple product offer reports the sale price at the offer top level.
+ *
+ * @return void
+ */
+ public function test_simple_product_offer_on_sale_uses_sale_price_at_offer_level(): void {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_regular_price( '100' );
+ $product->set_sale_price( '70' );
+ $product->set_price( '70' );
+ $product->save();
+
+ $this->structured_data->generate_product_data( $product );
+
+ $data = $this->structured_data->get_data();
+ $offer = $data[0]['offers'][0];
+
+ // The offer-level `price` should reflect the sale price, matching `priceSpecification[0]['price']`.
+ $this->assertEquals( '70.00', $offer['price'] );
+ $this->assertEquals( '70.00', $offer['priceSpecification'][0]['price'] );
+ $this->assertEquals( get_woocommerce_currency(), $offer['priceCurrency'] );
+ }
}