feat: Add product price comparison table

- Add "Load Product Comparison" button in admin
- Display WooCommerce products with local price (sale/regular)
- Show Google Merchant Center price for each product
- Show match type (offerId vs gtin) and match status
- Helps debug product matching issues
- Limited to 50 in-stock products for performance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 19:16:57 +01:00
parent bce0ccc0d4
commit 9172b923d9
2 changed files with 240 additions and 0 deletions

View File

@@ -51,6 +51,7 @@ class Informatiq_SP_Admin {
add_action( 'wp_ajax_informatiq_sp_manual_sync', array( $this, 'handle_manual_sync' ) ); add_action( 'wp_ajax_informatiq_sp_manual_sync', array( $this, 'handle_manual_sync' ) );
add_action( 'wp_ajax_informatiq_sp_test_connection', array( $this, 'handle_test_connection' ) ); 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_revoke_auth', array( $this, 'handle_revoke_auth' ) );
add_action( 'wp_ajax_informatiq_sp_compare_products', array( $this, 'handle_compare_products' ) );
// Enqueue admin assets. // Enqueue admin assets.
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
@@ -333,6 +334,10 @@ class Informatiq_SP_Admin {
<hr> <hr>
<?php $this->render_product_comparison(); ?>
<hr>
<?php $this->render_logs_section(); ?> <?php $this->render_logs_section(); ?>
</div> </div>
@@ -575,6 +580,50 @@ class Informatiq_SP_Admin {
<?php <?php
} }
/**
* Render product comparison section.
*/
private function render_product_comparison() {
?>
<div class="informatiq-sp-product-comparison">
<h2><?php esc_html_e( 'Product Price Comparison', 'informatiq-smart-pricing' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Compare WooCommerce product prices with Google Merchant Center prices.', 'informatiq-smart-pricing' ); ?>
</p>
<?php if ( $this->is_authorized() ) : ?>
<p>
<button type="button" class="button button-secondary" id="informatiq-sp-compare-products">
<?php esc_html_e( 'Load Product Comparison', 'informatiq-smart-pricing' ); ?>
</button>
<span class="spinner" style="float: none; margin-top: 0;"></span>
</p>
<div id="informatiq-sp-comparison-results" style="display: none;">
<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( 'Status', 'informatiq-smart-pricing' ); ?></th>
</tr>
</thead>
<tbody id="informatiq-sp-comparison-tbody">
</tbody>
</table>
</div>
<?php else : ?>
<p class="description" style="color: #d63638;">
<?php esc_html_e( 'Please authorize with Google first to compare products.', 'informatiq-smart-pricing' ); ?>
</p>
<?php endif; ?>
</div>
<?php
}
/** /**
* Render logs section. * Render logs section.
*/ */
@@ -752,6 +801,120 @@ class Informatiq_SP_Admin {
wp_send_json_success( array( 'message' => __( 'Authorization revoked.', 'informatiq-smart-pricing' ) ) ); wp_send_json_success( array( 'message' => __( 'Authorization revoked.', 'informatiq-smart-pricing' ) ) );
} }
/**
* Handle product comparison AJAX request.
*/
public function handle_compare_products() {
check_ajax_referer( 'informatiq_sp_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
$merchant_id = get_option( 'informatiq_sp_merchant_id' );
$client_id = get_option( 'informatiq_sp_client_id' );
$client_secret = get_option( 'informatiq_sp_client_secret' );
$refresh_token = get_option( 'informatiq_sp_refresh_token' );
if ( empty( $merchant_id ) || empty( $refresh_token ) ) {
wp_send_json_error( array( 'message' => __( 'Please configure settings and authorize first.', 'informatiq-smart-pricing' ) ) );
}
try {
$google_api = new Informatiq_SP_Google_API(
$merchant_id,
$client_id,
$client_secret,
$refresh_token,
$this->logger
);
// Get all Google products indexed by various identifiers.
$google_products_raw = $google_api->get_all_products();
$google_products = array();
foreach ( $google_products_raw as $gp ) {
// Index by offerId.
if ( ! empty( $gp['offerId'] ) ) {
$google_products['offer_' . $gp['offerId']] = $gp;
}
// Index by gtin.
if ( ! empty( $gp['gtin'] ) ) {
$google_products['gtin_' . $gp['gtin']] = $gp;
}
}
// Get WooCommerce in-stock products (limit to 50 for performance).
$wc_products = wc_get_products( array(
'status' => 'publish',
'stock_status' => 'instock',
'limit' => 50,
'return' => 'objects',
'type' => array( 'simple', 'variable' ),
) );
$comparison = array();
foreach ( $wc_products as $product ) {
$sku = $product->get_sku();
if ( empty( $sku ) ) {
continue;
}
// Get local price (sale price priority, then regular).
$sale_price = $product->get_sale_price();
$regular_price = $product->get_regular_price();
$local_price = ! empty( $sale_price ) ? $sale_price : $regular_price;
$price_type = ! empty( $sale_price ) ? 'sale' : 'regular';
// Try to find matching Google product.
$google_product = null;
$match_type = '';
// Try offerId match.
if ( 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';
}
// Get Google price.
$google_price = null;
if ( $google_product ) {
if ( isset( $google_product['price']['amountMicros'] ) ) {
$google_price = (float) $google_product['price']['amountMicros'] / 1000000;
}
}
$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'] ?? '' ) : '',
);
}
wp_send_json_success( array(
'products' => $comparison,
'google_count' => count( $google_products_raw ),
'wc_count' => count( $wc_products ),
'currency' => get_woocommerce_currency_symbol(),
) );
} catch ( Exception $e ) {
wp_send_json_error( array( 'message' => $e->getMessage() ) );
}
}
/** /**
* Enqueue admin assets. * Enqueue admin assets.
* *

View File

@@ -20,6 +20,7 @@
$('#informatiq-sp-manual-sync').on('click', this.handleManualSync); $('#informatiq-sp-manual-sync').on('click', this.handleManualSync);
$('#informatiq-sp-test-connection').on('click', this.handleTestConnection); $('#informatiq-sp-test-connection').on('click', this.handleTestConnection);
$('#informatiq-sp-revoke-auth').on('click', this.handleRevokeAuth); $('#informatiq-sp-revoke-auth').on('click', this.handleRevokeAuth);
$('#informatiq-sp-compare-products').on('click', this.handleCompareProducts);
}, },
/** /**
@@ -179,6 +180,82 @@
$button.prop('disabled', false).text('Revoke Authorization'); $button.prop('disabled', false).text('Revoke Authorization');
} }
}); });
},
/**
* Handle compare products button click
*/
handleCompareProducts: function(e) {
e.preventDefault();
var $button = $(this);
var $spinner = $button.next('.spinner');
var $results = $('#informatiq-sp-comparison-results');
var $tbody = $('#informatiq-sp-comparison-tbody');
// Disable button and show spinner
$button.prop('disabled', true);
$spinner.addClass('is-active');
// Make AJAX request
$.ajax({
url: informatiqSP.ajaxUrl,
type: 'POST',
data: {
action: 'informatiq_sp_compare_products',
nonce: informatiqSP.nonce
},
success: function(response) {
if (response.success) {
var data = response.data;
var html = '';
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';
if (product.found && product.google_offer) {
matchLabel += ' <small>(' + product.google_offer + ')</small>';
}
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 += '</tr>';
});
}
$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>');
} else {
alert('Error: ' + (response.data.message || 'Unknown error'));
}
},
error: function(jqXHR, textStatus, errorThrown) {
alert('Error: ' + errorThrown);
},
complete: function() {
$button.prop('disabled', false);
$spinner.removeClass('is-active');
}
});
} }
}; };