Implement Google Price Insights with update functionality

Major rewrite using correct Google Merchant API:
- Use price_insights_product_view table (correct API endpoint)
- Fetch suggested_price and predicted performance changes
- Show predicted impact on impressions, clicks, conversions

New features:
- Individual "Update" button per product
- Bulk update with checkbox selection
- Pagination (50 products per page)
- Sort by potential gain (highest first)

Price handling:
- Always use tax-inclusive prices for comparison with Google
- Convert back to store format when saving (handles tax-exclusive stores)
- Set as sale price when updating

UI improvements:
- Color-coded gain/loss values
- Color-coded predicted changes
- Summary stats showing products that can increase/decrease
- Total potential gain calculation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 18:22:17 +01:00
parent 1b12eb53d0
commit 53185fd49e
3 changed files with 546 additions and 322 deletions

View File

@@ -457,27 +457,32 @@ class Informatiq_SP_Google_API {
}
/**
* Get all benchmark prices from Price Competitiveness Report.
* Get price insights from Google's price_insights_product_view.
*
* @return array Associative array of offer_id => benchmark_price.
* Returns suggested prices and predicted performance impact.
*
* @return array Associative array of offer_id => price insight data.
*/
public function get_all_benchmark_prices() {
public function get_price_insights() {
try {
$endpoint = "/reports/v1beta/accounts/{$this->merchant_id}/reports:search";
$all_benchmarks = array();
$all_insights = array();
$page_token = null;
do {
// New Merchant API uses snake_case table names.
// Fields from product are prefixed with product_view.
$query = array(
'query' => "SELECT
product_view.offer_id,
product_view.gtin,
product_view.title,
benchmark_price
FROM price_competitiveness_product_view",
id,
offer_id,
title,
brand,
price,
suggested_price,
predicted_impressions_change_fraction,
predicted_clicks_change_fraction,
predicted_conversions_change_fraction
FROM price_insights_product_view",
);
if ( $page_token ) {
@@ -486,53 +491,56 @@ class Informatiq_SP_Google_API {
$response = $this->api_request( 'POST', $endpoint, $query );
$this->logger->info( 'Benchmark API response keys: ' . wp_json_encode( array_keys( $response ) ) );
if ( ! empty( $response['results'][0] ) ) {
$this->logger->info( 'Sample benchmark result: ' . wp_json_encode( $response['results'][0] ) );
// Log first result for debugging.
if ( empty( $all_insights ) && ! empty( $response['results'][0] ) ) {
$this->logger->info( 'Sample price insight: ' . wp_json_encode( $response['results'][0] ) );
}
if ( ! empty( $response['results'] ) ) {
foreach ( $response['results'] as $result ) {
// Extract data - response nests product fields under productView.
$product_view = $result['productView'] ?? array();
$offer_id = $product_view['offerId'] ?? ( $product_view['offer_id'] ?? null );
$gtin = $product_view['gtin'] ?? null;
$title = $product_view['title'] ?? '';
$insight = $result['priceInsightsProductView'] ?? $result;
$benchmark_price = null;
// Try different possible response structures for benchmark.
if ( isset( $result['benchmarkPrice']['amountMicros'] ) ) {
$benchmark_price = (float) $result['benchmarkPrice']['amountMicros'] / 1000000;
} elseif ( isset( $result['benchmark_price']['amountMicros'] ) ) {
$benchmark_price = (float) $result['benchmark_price']['amountMicros'] / 1000000;
} elseif ( isset( $result['priceBenchmark']['priceBenchmarkValue']['amountMicros'] ) ) {
$benchmark_price = (float) $result['priceBenchmark']['priceBenchmarkValue']['amountMicros'] / 1000000;
$offer_id = $insight['offerId'] ?? ( $insight['offer_id'] ?? null );
$product_id = $insight['id'] ?? null;
// Extract offer_id from product_id if needed (format: lang~country~offerId).
if ( ! $offer_id && $product_id ) {
$parts = explode( '~', $product_id );
if ( count( $parts ) >= 3 ) {
$offer_id = $parts[ count( $parts ) - 1 ];
}
}
$own_price = null;
if ( isset( $product_view['price']['amountMicros'] ) ) {
$own_price = (float) $product_view['price']['amountMicros'] / 1000000;
} elseif ( isset( $result['price']['amountMicros'] ) ) {
$own_price = (float) $result['price']['amountMicros'] / 1000000;
$current_price = null;
if ( isset( $insight['price']['amountMicros'] ) ) {
$current_price = (float) $insight['price']['amountMicros'] / 1000000;
}
if ( $offer_id && $benchmark_price ) {
$all_benchmarks[ 'offer_' . $offer_id ] = array(
'benchmark_price' => $benchmark_price,
'own_price' => $own_price,
'title' => $title,
'gtin' => $gtin,
$suggested_price = null;
if ( isset( $insight['suggestedPrice']['amountMicros'] ) ) {
$suggested_price = (float) $insight['suggestedPrice']['amountMicros'] / 1000000;
}
if ( $offer_id ) {
$data = array(
'offer_id' => $offer_id,
'product_id' => $product_id,
'title' => $insight['title'] ?? '',
'brand' => $insight['brand'] ?? '',
'google_price' => $current_price,
'suggested_price' => $suggested_price,
'predicted_impressions_change' => isset( $insight['predictedImpressionsChangeFraction'] )
? (float) $insight['predictedImpressionsChangeFraction'] * 100
: null,
'predicted_clicks_change' => isset( $insight['predictedClicksChangeFraction'] )
? (float) $insight['predictedClicksChangeFraction'] * 100
: null,
'predicted_conversions_change' => isset( $insight['predictedConversionsChangeFraction'] )
? (float) $insight['predictedConversionsChangeFraction'] * 100
: null,
);
// Also index by GTIN if available.
if ( $gtin ) {
$all_benchmarks[ 'gtin_' . $gtin ] = array(
'benchmark_price' => $benchmark_price,
'own_price' => $own_price,
'title' => $title,
'offer_id' => $offer_id,
);
}
$all_insights[ 'offer_' . $offer_id ] = $data;
}
}
}
@@ -541,12 +549,12 @@ class Informatiq_SP_Google_API {
} while ( $page_token );
$this->logger->info( sprintf( 'Fetched benchmark prices for %d products', count( $all_benchmarks ) ) );
$this->logger->info( sprintf( 'Fetched price insights for %d products', count( $all_insights ) ) );
return $all_benchmarks;
return $all_insights;
} catch ( Exception $e ) {
$this->logger->error( 'Error fetching benchmark prices: ' . $e->getMessage() );
$this->logger->error( 'Error fetching price insights: ' . $e->getMessage() );
return array();
}
}