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

@@ -52,6 +52,8 @@ class Informatiq_SP_Admin {
add_action( 'wp_ajax_informatiq_sp_test_connection', array( $this, 'handle_test_connection' ) );
add_action( 'wp_ajax_informatiq_sp_revoke_auth', array( $this, 'handle_revoke_auth' ) );
add_action( 'wp_ajax_informatiq_sp_compare_products', array( $this, 'handle_compare_products' ) );
add_action( 'wp_ajax_informatiq_sp_update_price', array( $this, 'handle_update_price' ) );
add_action( 'wp_ajax_informatiq_sp_bulk_update_prices', array( $this, 'handle_bulk_update_prices' ) );
// Enqueue admin assets.
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
@@ -601,20 +603,42 @@ class Informatiq_SP_Admin {
<div id="informatiq-sp-comparison-results" style="display: none;">
<div id="informatiq-sp-comparison-summary" class="notice notice-info" style="margin: 10px 0; padding: 10px;"></div>
<table class="widefat striped">
<p style="margin: 10px 0;">
<button type="button" class="button button-primary" id="informatiq-sp-bulk-update" disabled>
<?php esc_html_e( 'Bulk Update Selected', 'informatiq-smart-pricing' ); ?>
</button>
<label style="margin-left: 15px;">
<input type="checkbox" id="informatiq-sp-select-all">
<?php esc_html_e( 'Select all with suggestions', 'informatiq-smart-pricing' ); ?>
</label>
</p>
<table class="widefat striped" id="informatiq-sp-comparison-table">
<thead>
<tr>
<th style="width: 30px;"><input type="checkbox" id="informatiq-sp-select-all-header"></th>
<th><?php esc_html_e( 'Product', 'informatiq-smart-pricing' ); ?></th>
<th><?php esc_html_e( 'Your Price', 'informatiq-smart-pricing' ); ?></th>
<th><?php esc_html_e( 'Competitor', 'informatiq-smart-pricing' ); ?></th>
<th><?php esc_html_e( 'Recommended', 'informatiq-smart-pricing' ); ?></th>
<th><?php esc_html_e( 'Potential +/-', 'informatiq-smart-pricing' ); ?></th>
<th><?php esc_html_e( 'Status', 'informatiq-smart-pricing' ); ?></th>
<th><?php esc_html_e( 'Suggested', 'informatiq-smart-pricing' ); ?></th>
<th><?php esc_html_e( 'Gain/Loss', 'informatiq-smart-pricing' ); ?></th>
<th title="<?php esc_attr_e( 'Predicted change in impressions', 'informatiq-smart-pricing' ); ?>">
<?php esc_html_e( 'Impr.', 'informatiq-smart-pricing' ); ?>
</th>
<th title="<?php esc_attr_e( 'Predicted change in clicks', 'informatiq-smart-pricing' ); ?>">
<?php esc_html_e( 'Clicks', 'informatiq-smart-pricing' ); ?>
</th>
<th title="<?php esc_attr_e( 'Predicted change in conversions', 'informatiq-smart-pricing' ); ?>">
<?php esc_html_e( 'Conv.', 'informatiq-smart-pricing' ); ?>
</th>
<th><?php esc_html_e( 'Action', 'informatiq-smart-pricing' ); ?></th>
</tr>
</thead>
<tbody id="informatiq-sp-comparison-tbody">
</tbody>
</table>
<div id="informatiq-sp-pagination" style="margin-top: 15px; text-align: center;"></div>
</div>
<?php else : ?>
<p class="description" style="color: #d63638;">
@@ -830,40 +854,15 @@ class Informatiq_SP_Admin {
$this->logger
);
// Get benchmark prices from Price Competitiveness Report.
$benchmark_data = $google_api->get_all_benchmark_prices();
$this->logger->info( sprintf( 'Loaded %d benchmark prices from Google', count( $benchmark_data ) ) );
// Get price insights from Google (suggested prices + predicted impact).
$price_insights = $google_api->get_price_insights();
$this->logger->info( sprintf( 'Loaded %d price insights from Google', count( $price_insights ) ) );
// Also get all products for matching purposes.
$google_products_raw = $google_api->get_all_products();
$google_products = array();
foreach ( $google_products_raw as $gp ) {
// Index by offerId (top-level field).
if ( ! empty( $gp['offerId'] ) ) {
$google_products['offer_' . $gp['offerId']] = $gp;
}
// Index by GTIN - check attributes.gtin which is an ARRAY.
if ( ! empty( $gp['attributes']['gtin'] ) && is_array( $gp['attributes']['gtin'] ) ) {
foreach ( $gp['attributes']['gtin'] as $gtin_value ) {
$google_products['gtin_' . $gtin_value] = $gp;
}
}
// Also check attributes.gtins (alternative field name).
if ( ! empty( $gp['attributes']['gtins'] ) && is_array( $gp['attributes']['gtins'] ) ) {
foreach ( $gp['attributes']['gtins'] as $gtin_value ) {
$google_products['gtin_' . $gtin_value] = $gp;
}
}
}
// Get WooCommerce in-stock products (limit to 50 for performance).
// Get all WooCommerce in-stock products (no limit - load all).
$wc_products = wc_get_products( array(
'status' => 'publish',
'stock_status' => 'instock',
'limit' => 50,
'limit' => -1,
'return' => 'objects',
'type' => array( 'simple', 'variable' ),
) );
@@ -876,115 +875,66 @@ class Informatiq_SP_Admin {
continue;
}
// Get local price (sale price priority, then regular).
// Always calculate tax-inclusive price for comparison with Google (which is always tax-inclusive).
$product_id = $product->get_id();
// Get local price (tax-inclusive for comparison with Google).
$sale_price = $product->get_sale_price();
$regular_price = $product->get_regular_price();
$base_price = ! empty( $sale_price ) ? $sale_price : $regular_price;
$price_type = ! empty( $sale_price ) ? 'sale' : 'regular';
// Get price including tax for fair comparison with Google.
$local_price = null;
$local_price_incl_tax = null;
if ( $base_price ) {
$local_price = wc_get_price_including_tax( $product, array( 'price' => $base_price ) );
$local_price_incl_tax = wc_get_price_including_tax( $product, array( 'price' => $base_price ) );
}
// Try to find matching Google product and benchmark.
$google_product = null;
$benchmark_info = null;
$match_type = '';
$product_id = $product->get_id();
// Try offerId match (using WooCommerce product ID).
if ( isset( $google_products['offer_' . $product_id] ) ) {
$google_product = $google_products['offer_' . $product_id];
$match_type = 'product_id';
}
// Try offerId match with SKU.
elseif ( isset( $google_products['offer_' . $sku] ) ) {
$google_product = $google_products['offer_' . $sku];
$match_type = 'offerId';
}
// Try GTIN match (when SKU is a barcode).
elseif ( isset( $google_products['gtin_' . $sku] ) ) {
$google_product = $google_products['gtin_' . $sku];
$match_type = 'gtin';
// Try to find matching price insight by WooCommerce product ID (which is Google's offerId).
$insight = null;
if ( isset( $price_insights[ 'offer_' . $product_id ] ) ) {
$insight = $price_insights[ 'offer_' . $product_id ];
}
// Get benchmark data (competitor price).
if ( isset( $benchmark_data['offer_' . $product_id] ) ) {
$benchmark_info = $benchmark_data['offer_' . $product_id];
} elseif ( isset( $benchmark_data['offer_' . $sku] ) ) {
$benchmark_info = $benchmark_data['offer_' . $sku];
} elseif ( isset( $benchmark_data['gtin_' . $sku] ) ) {
$benchmark_info = $benchmark_data['gtin_' . $sku];
}
// Calculate potential gain and determine if update is beneficial.
$suggested_price = $insight['suggested_price'] ?? null;
$potential_gain = null;
$should_update = false;
// Get Google's own price from product data.
$google_own_price = null;
if ( $google_product ) {
if ( isset( $google_product['attributes']['price']['amountMicros'] ) ) {
$google_own_price = (float) $google_product['attributes']['price']['amountMicros'] / 1000000;
}
}
// Get competitor benchmark price.
$benchmark_price = $benchmark_info['benchmark_price'] ?? null;
// Calculate recommended price (slightly below competitor).
$recommended_price = null;
$potential_gain = null;
$price_status = 'unknown';
if ( $benchmark_price && $local_price ) {
// Recommended: 20 cents below competitor (or 0.5% whichever is less).
$offset = min( 0.20, $benchmark_price * 0.005 );
$recommended_price = round( $benchmark_price - $offset, 2 );
// Calculate potential gain per unit.
$potential_gain = round( $recommended_price - $local_price, 2 );
// Determine status.
if ( $local_price < $benchmark_price * 0.98 ) {
$price_status = 'cheaper'; // More than 2% cheaper - could increase.
} elseif ( $local_price <= $benchmark_price ) {
$price_status = 'competitive'; // Within 2% below - good.
} else {
$price_status = 'expensive'; // Above competitor.
}
if ( $suggested_price && $local_price_incl_tax ) {
$potential_gain = round( $suggested_price - $local_price_incl_tax, 2 );
// Suggest update if Google recommends a different price.
$should_update = abs( $potential_gain ) > 0.05;
}
$comparison[] = array(
'id' => $product_id,
'name' => $product->get_name(),
'sku' => $sku,
'local_price' => $local_price ? (float) $local_price : null,
'price_type' => $price_type,
'google_own_price' => $google_own_price,
'benchmark_price' => $benchmark_price,
'recommended_price' => $recommended_price,
'potential_gain' => $potential_gain,
'price_status' => $price_status,
'match_type' => $match_type,
'found' => ! empty( $google_product ),
'has_benchmark' => ! empty( $benchmark_price ),
'google_offer' => $google_product ? ( $google_product['offerId'] ?? '' ) : '',
'id' => $product_id,
'name' => $product->get_name(),
'sku' => $sku,
'local_price' => $local_price_incl_tax ? round( $local_price_incl_tax, 2 ) : null,
'price_type' => $price_type,
'google_price' => $insight['google_price'] ?? null,
'suggested_price' => $suggested_price,
'potential_gain' => $potential_gain,
'predicted_impressions_change' => $insight['predicted_impressions_change'] ?? null,
'predicted_clicks_change' => $insight['predicted_clicks_change'] ?? null,
'predicted_conversions_change' => $insight['predicted_conversions_change'] ?? null,
'has_insight' => ! empty( $insight ),
'should_update' => $should_update,
);
}
// Get sample of indexed keys for debugging.
$sample_keys = array_slice( array_keys( $google_products ), 0, 10 );
// Sort by potential gain (highest first).
usort( $comparison, function( $a, $b ) {
$gain_a = $a['potential_gain'] ?? 0;
$gain_b = $b['potential_gain'] ?? 0;
return $gain_b <=> $gain_a;
} );
wp_send_json_success( array(
'products' => $comparison,
'google_count' => count( $google_products_raw ),
'wc_count' => count( $wc_products ),
'currency' => get_woocommerce_currency_symbol(),
'debug' => array(
'sample_google_keys' => $sample_keys,
'sample_google_product' => ! empty( $google_products_raw[0] ) ? $google_products_raw[0] : null,
'index_count' => count( $google_products ),
),
'products' => $comparison,
'insights_count' => count( $price_insights ),
'wc_count' => count( $wc_products ),
'currency' => get_woocommerce_currency_symbol(),
'currency_code' => get_woocommerce_currency(),
) );
} catch ( Exception $e ) {
@@ -992,6 +942,147 @@ class Informatiq_SP_Admin {
}
}
/**
* Handle individual price update AJAX request.
*/
public function handle_update_price() {
check_ajax_referer( 'informatiq_sp_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
$new_price = isset( $_POST['new_price'] ) ? floatval( $_POST['new_price'] ) : 0;
if ( ! $product_id || $new_price <= 0 ) {
wp_send_json_error( array( 'message' => __( 'Invalid product ID or price.', 'informatiq-smart-pricing' ) ) );
}
$product = wc_get_product( $product_id );
if ( ! $product ) {
wp_send_json_error( array( 'message' => __( 'Product not found.', 'informatiq-smart-pricing' ) ) );
}
// Convert tax-inclusive price to the format WooCommerce expects.
$price_to_set = $new_price;
if ( ! wc_prices_include_tax() ) {
// Store prices exclude tax, so we need to reverse-calculate.
$tax_rates = WC_Tax::get_rates( $product->get_tax_class() );
if ( ! empty( $tax_rates ) ) {
$tax_rate = 0;
foreach ( $tax_rates as $rate ) {
$tax_rate += (float) $rate['rate'];
}
$price_to_set = $new_price / ( 1 + ( $tax_rate / 100 ) );
}
}
$price_to_set = round( $price_to_set, wc_get_price_decimals() );
// Store old price for logging.
$old_price = $product->get_sale_price() ?: $product->get_regular_price();
// Set as sale price (since we're adjusting to be competitive).
$product->set_sale_price( $price_to_set );
$product->save();
// Log the update.
$this->logger->log_price_update(
$product_id,
$product->get_name(),
$old_price,
$price_to_set,
$new_price,
'Manual update to Google suggested price'
);
wp_send_json_success( array(
'message' => sprintf(
__( 'Price updated for %s: %s', 'informatiq-smart-pricing' ),
$product->get_name(),
wc_price( $price_to_set )
),
'new_price' => $price_to_set,
) );
}
/**
* Handle bulk price update AJAX request.
*/
public function handle_bulk_update_prices() {
check_ajax_referer( 'informatiq_sp_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
$updates = isset( $_POST['updates'] ) ? $_POST['updates'] : array();
if ( empty( $updates ) || ! is_array( $updates ) ) {
wp_send_json_error( array( 'message' => __( 'No updates provided.', 'informatiq-smart-pricing' ) ) );
}
$updated = 0;
$errors = 0;
foreach ( $updates as $update ) {
$product_id = absint( $update['product_id'] ?? 0 );
$new_price = floatval( $update['new_price'] ?? 0 );
if ( ! $product_id || $new_price <= 0 ) {
$errors++;
continue;
}
$product = wc_get_product( $product_id );
if ( ! $product ) {
$errors++;
continue;
}
// Convert tax-inclusive price to the format WooCommerce expects.
$price_to_set = $new_price;
if ( ! wc_prices_include_tax() ) {
$tax_rates = WC_Tax::get_rates( $product->get_tax_class() );
if ( ! empty( $tax_rates ) ) {
$tax_rate = 0;
foreach ( $tax_rates as $rate ) {
$tax_rate += (float) $rate['rate'];
}
$price_to_set = $new_price / ( 1 + ( $tax_rate / 100 ) );
}
}
$price_to_set = round( $price_to_set, wc_get_price_decimals() );
$old_price = $product->get_sale_price() ?: $product->get_regular_price();
$product->set_sale_price( $price_to_set );
$product->save();
$this->logger->log_price_update(
$product_id,
$product->get_name(),
$old_price,
$price_to_set,
$new_price,
'Bulk update to Google suggested price'
);
$updated++;
}
wp_send_json_success( array(
'message' => sprintf(
__( 'Bulk update complete: %d updated, %d errors.', 'informatiq-smart-pricing' ),
$updated,
$errors
),
'updated' => $updated,
'errors' => $errors,
) );
}
/**
* Enqueue admin assets.
*