Add competitor benchmark pricing and profit optimization
Features: - Fetch competitor benchmark prices from Google Price Competitiveness Report - New get_all_benchmark_prices() method in Google API class - Display competitor price instead of own Google price - Calculate recommended price (slightly below competitor) - Show potential gain/loss per product if price is optimized - Color-coded status: - Green: Your price is cheaper (opportunity to increase) - Blue: Competitive (within 2% of competitor) - Red: Expensive (above competitor) - Summary statistics showing: - Products with benchmark data - Count by status (cheaper/competitive/expensive) - Total potential gain if all prices optimized Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -600,14 +600,15 @@ class Informatiq_SP_Admin {
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Product', 'informatiq-smart-pricing' ); ?></th>
|
||||
<th><?php esc_html_e( 'SKU', 'informatiq-smart-pricing' ); ?></th>
|
||||
<th><?php esc_html_e( 'Local Price', 'informatiq-smart-pricing' ); ?></th>
|
||||
<th><?php esc_html_e( 'Google Price', 'informatiq-smart-pricing' ); ?></th>
|
||||
<th><?php esc_html_e( 'Google Match', '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>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -829,16 +830,14 @@ class Informatiq_SP_Admin {
|
||||
$this->logger
|
||||
);
|
||||
|
||||
// Get all Google products indexed by various identifiers.
|
||||
// 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 ) ) );
|
||||
|
||||
// Also get all products for matching purposes.
|
||||
$google_products_raw = $google_api->get_all_products();
|
||||
$google_products = array();
|
||||
|
||||
// Log first product structure for debugging.
|
||||
if ( ! empty( $google_products_raw[0] ) ) {
|
||||
$this->logger->info( 'Sample Google product structure: ' . wp_json_encode( array_keys( $google_products_raw[0] ) ) );
|
||||
$this->logger->info( 'Sample Google product data: ' . wp_json_encode( $google_products_raw[0] ) );
|
||||
}
|
||||
|
||||
foreach ( $google_products_raw as $gp ) {
|
||||
// Index by offerId (top-level field).
|
||||
if ( ! empty( $gp['offerId'] ) ) {
|
||||
@@ -858,11 +857,6 @@ class Informatiq_SP_Admin {
|
||||
$google_products['gtin_' . $gtin_value] = $gp;
|
||||
}
|
||||
}
|
||||
|
||||
// Index by MPN if available.
|
||||
if ( ! empty( $gp['attributes']['mpn'] ) ) {
|
||||
$google_products['mpn_' . $gp['attributes']['mpn']] = $gp;
|
||||
}
|
||||
}
|
||||
|
||||
// Get WooCommerce in-stock products (limit to 50 for performance).
|
||||
@@ -895,12 +889,19 @@ class Informatiq_SP_Admin {
|
||||
$local_price = wc_get_price_including_tax( $product, array( 'price' => $base_price ) );
|
||||
}
|
||||
|
||||
// Try to find matching Google product.
|
||||
// Try to find matching Google product and benchmark.
|
||||
$google_product = null;
|
||||
$benchmark_info = null;
|
||||
$match_type = '';
|
||||
$product_id = $product->get_id();
|
||||
|
||||
// Try offerId match.
|
||||
if ( isset( $google_products['offer_' . $sku] ) ) {
|
||||
// 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';
|
||||
}
|
||||
@@ -910,26 +911,64 @@ class Informatiq_SP_Admin {
|
||||
$match_type = 'gtin';
|
||||
}
|
||||
|
||||
// Get Google price (nested in attributes in new Merchant API).
|
||||
$google_price = null;
|
||||
// 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];
|
||||
}
|
||||
|
||||
// Get Google's own price from product data.
|
||||
$google_own_price = null;
|
||||
if ( $google_product ) {
|
||||
if ( isset( $google_product['attributes']['price']['amountMicros'] ) ) {
|
||||
$google_price = (float) $google_product['attributes']['price']['amountMicros'] / 1000000;
|
||||
} elseif ( isset( $google_product['price']['amountMicros'] ) ) {
|
||||
$google_price = (float) $google_product['price']['amountMicros'] / 1000000;
|
||||
$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.
|
||||
}
|
||||
}
|
||||
|
||||
$comparison[] = array(
|
||||
'id' => $product->get_id(),
|
||||
'name' => $product->get_name(),
|
||||
'sku' => $sku,
|
||||
'local_price' => $local_price ? (float) $local_price : null,
|
||||
'price_type' => $price_type,
|
||||
'google_price' => $google_price,
|
||||
'match_type' => $match_type,
|
||||
'found' => ! empty( $google_product ),
|
||||
'google_offer' => $google_product ? ( $google_product['offerId'] ?? '' ) : '',
|
||||
'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'] ?? '' ) : '',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -259,28 +259,82 @@ console.log('Informatiq Smart Pricing: Script loaded');
|
||||
var data = response.data;
|
||||
var html = '';
|
||||
|
||||
// Stats counters.
|
||||
var stats = {
|
||||
total: 0,
|
||||
withBenchmark: 0,
|
||||
cheaper: 0,
|
||||
competitive: 0,
|
||||
expensive: 0,
|
||||
totalPotentialGain: 0
|
||||
};
|
||||
|
||||
if (data.products.length === 0) {
|
||||
html = '<tr><td colspan="6">No in-stock products with SKU found.</td></tr>';
|
||||
} else {
|
||||
$.each(data.products, function(i, product) {
|
||||
var localPrice = product.local_price ? data.currency + parseFloat(product.local_price).toFixed(2) : '-';
|
||||
var googlePrice = product.google_price ? data.currency + parseFloat(product.google_price).toFixed(2) : '-';
|
||||
var priceLabel = product.price_type === 'sale' ? ' <small>(sale)</small>' : ' <small>(regular)</small>';
|
||||
var matchLabel = product.match_type ? product.match_type : '-';
|
||||
var statusClass = product.found ? 'color: #00a32a;' : 'color: #d63638;';
|
||||
var statusText = product.found ? 'Found' : 'Not Found';
|
||||
stats.total++;
|
||||
|
||||
if (product.found && product.google_offer) {
|
||||
matchLabel += ' <small>(' + product.google_offer + ')</small>';
|
||||
var localPrice = product.local_price ? data.currency + parseFloat(product.local_price).toFixed(2) : '-';
|
||||
var benchmarkPrice = product.benchmark_price ? data.currency + parseFloat(product.benchmark_price).toFixed(2) : '-';
|
||||
var recommendedPrice = product.recommended_price ? data.currency + parseFloat(product.recommended_price).toFixed(2) : '-';
|
||||
var potentialGain = '-';
|
||||
var priceLabel = product.price_type === 'sale' ? ' <small>(sale)</small>' : '';
|
||||
|
||||
// Status and colors.
|
||||
var statusText = '-';
|
||||
var statusStyle = '';
|
||||
var localPriceStyle = '';
|
||||
var gainStyle = '';
|
||||
|
||||
if (product.has_benchmark) {
|
||||
stats.withBenchmark++;
|
||||
|
||||
if (product.price_status === 'cheaper') {
|
||||
stats.cheaper++;
|
||||
statusText = 'Cheaper';
|
||||
statusStyle = 'color: #00a32a; font-weight: bold;';
|
||||
localPriceStyle = 'color: #00a32a;';
|
||||
} else if (product.price_status === 'competitive') {
|
||||
stats.competitive++;
|
||||
statusText = 'Competitive';
|
||||
statusStyle = 'color: #2271b1; font-weight: bold;';
|
||||
localPriceStyle = 'color: #2271b1;';
|
||||
} else if (product.price_status === 'expensive') {
|
||||
stats.expensive++;
|
||||
statusText = 'Expensive';
|
||||
statusStyle = 'color: #d63638; font-weight: bold;';
|
||||
localPriceStyle = 'color: #d63638;';
|
||||
}
|
||||
|
||||
if (product.potential_gain !== null) {
|
||||
var gain = parseFloat(product.potential_gain);
|
||||
stats.totalPotentialGain += gain;
|
||||
if (gain > 0) {
|
||||
potentialGain = '+' + data.currency + gain.toFixed(2);
|
||||
gainStyle = 'color: #00a32a; font-weight: bold;';
|
||||
} else if (gain < 0) {
|
||||
potentialGain = '-' + data.currency + Math.abs(gain).toFixed(2);
|
||||
gainStyle = 'color: #d63638;';
|
||||
} else {
|
||||
potentialGain = data.currency + '0.00';
|
||||
}
|
||||
}
|
||||
} else if (product.found) {
|
||||
statusText = 'No benchmark';
|
||||
statusStyle = 'color: #dba617;';
|
||||
} else {
|
||||
statusText = 'Not in Google';
|
||||
statusStyle = 'color: #888;';
|
||||
}
|
||||
|
||||
html += '<tr>';
|
||||
html += '<td><a href="post.php?post=' + product.id + '&action=edit">' + product.name + '</a></td>';
|
||||
html += '<td><code>' + product.sku + '</code></td>';
|
||||
html += '<td>' + localPrice + priceLabel + '</td>';
|
||||
html += '<td>' + googlePrice + '</td>';
|
||||
html += '<td>' + matchLabel + '</td>';
|
||||
html += '<td style="' + statusClass + '"><strong>' + statusText + '</strong></td>';
|
||||
html += '<td><a href="post.php?post=' + product.id + '&action=edit" title="SKU: ' + product.sku + '">' + product.name + '</a></td>';
|
||||
html += '<td style="' + localPriceStyle + '">' + localPrice + priceLabel + '</td>';
|
||||
html += '<td>' + benchmarkPrice + '</td>';
|
||||
html += '<td>' + recommendedPrice + '</td>';
|
||||
html += '<td style="' + gainStyle + '">' + potentialGain + '</td>';
|
||||
html += '<td style="' + statusStyle + '">' + statusText + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
}
|
||||
@@ -288,10 +342,19 @@ console.log('Informatiq Smart Pricing: Script loaded');
|
||||
$tbody.html(html);
|
||||
$results.show();
|
||||
|
||||
// Show summary
|
||||
var summary = 'Showing ' + data.products.length + ' WooCommerce products. ';
|
||||
summary += data.google_count + ' products found in Google Merchant Center.';
|
||||
$button.after('<p class="description" style="margin-top: 10px;">' + summary + '</p>');
|
||||
// Show summary with statistics.
|
||||
var $summary = $('#informatiq-sp-comparison-summary');
|
||||
var summaryHtml = '<strong>Summary:</strong> ';
|
||||
summaryHtml += stats.withBenchmark + ' of ' + stats.total + ' products have competitor data. ';
|
||||
summaryHtml += '<span style="color:#00a32a;">' + stats.cheaper + ' cheaper</span>, ';
|
||||
summaryHtml += '<span style="color:#2271b1;">' + stats.competitive + ' competitive</span>, ';
|
||||
summaryHtml += '<span style="color:#d63638;">' + stats.expensive + ' expensive</span>. ';
|
||||
if (stats.totalPotentialGain > 0) {
|
||||
summaryHtml += '<br><strong style="color:#00a32a;">Total potential gain if optimized: +' + data.currency + stats.totalPotentialGain.toFixed(2) + ' per sale</strong>';
|
||||
} else if (stats.totalPotentialGain < 0) {
|
||||
summaryHtml += '<br><strong>Current margin vs recommended: ' + data.currency + stats.totalPotentialGain.toFixed(2) + '</strong>';
|
||||
}
|
||||
$summary.html(summaryHtml).show();
|
||||
|
||||
} else {
|
||||
$status
|
||||
|
||||
@@ -458,6 +458,88 @@ class Informatiq_SP_Google_API {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all benchmark prices from Price Competitiveness Report.
|
||||
*
|
||||
* @return array Associative array of offer_id => benchmark_price.
|
||||
*/
|
||||
public function get_all_benchmark_prices() {
|
||||
try {
|
||||
$endpoint = "/reports/v1beta/accounts/{$this->merchant_id}/reports:search";
|
||||
|
||||
$all_benchmarks = array();
|
||||
$page_token = null;
|
||||
|
||||
do {
|
||||
$query = array(
|
||||
'query' => "SELECT
|
||||
offer_id,
|
||||
product_view.gtin,
|
||||
price_benchmark.price_benchmark_value,
|
||||
price_benchmark.price_benchmark_currency_code,
|
||||
product_view.price,
|
||||
product_view.title
|
||||
FROM PriceCompetitivenessProductView",
|
||||
);
|
||||
|
||||
if ( $page_token ) {
|
||||
$query['pageToken'] = $page_token;
|
||||
}
|
||||
|
||||
$response = $this->api_request( 'POST', $endpoint, $query );
|
||||
|
||||
if ( ! empty( $response['results'] ) ) {
|
||||
foreach ( $response['results'] as $result ) {
|
||||
$offer_id = $result['offerId'] ?? null;
|
||||
$gtin = $result['productView']['gtin'] ?? null;
|
||||
|
||||
$benchmark_price = null;
|
||||
if ( isset( $result['priceBenchmark']['priceBenchmarkValue']['amountMicros'] ) ) {
|
||||
$benchmark_price = (float) $result['priceBenchmark']['priceBenchmarkValue']['amountMicros'] / 1000000;
|
||||
}
|
||||
|
||||
$own_price = null;
|
||||
if ( isset( $result['productView']['price']['amountMicros'] ) ) {
|
||||
$own_price = (float) $result['productView']['price']['amountMicros'] / 1000000;
|
||||
}
|
||||
|
||||
$title = $result['productView']['title'] ?? '';
|
||||
|
||||
if ( $offer_id && $benchmark_price ) {
|
||||
$all_benchmarks[ 'offer_' . $offer_id ] = array(
|
||||
'benchmark_price' => $benchmark_price,
|
||||
'own_price' => $own_price,
|
||||
'title' => $title,
|
||||
'gtin' => $gtin,
|
||||
);
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$page_token = $response['nextPageToken'] ?? null;
|
||||
|
||||
} while ( $page_token );
|
||||
|
||||
$this->logger->info( sprintf( 'Fetched benchmark prices for %d products', count( $all_benchmarks ) ) );
|
||||
|
||||
return $all_benchmarks;
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
$this->logger->error( 'Error fetching benchmark prices: ' . $e->getMessage() );
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test API connection.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user