logger = $logger;
$this->price_updater = $price_updater;
// Add admin menu.
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
// Register settings.
add_action( 'admin_init', array( $this, 'register_settings' ) );
// Handle OAuth callback.
add_action( 'admin_init', array( $this, 'handle_oauth_callback' ) );
// Handle AJAX requests.
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_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' ) );
}
/**
* Add admin menu under WooCommerce.
*/
public function add_admin_menu() {
add_submenu_page(
'woocommerce',
__( 'Smart Pricing', 'informatiq-smart-pricing' ),
__( 'Smart Pricing', 'informatiq-smart-pricing' ),
'manage_woocommerce',
'informatiq-smart-pricing',
array( $this, 'render_admin_page' )
);
}
/**
* Register plugin settings.
*/
public function register_settings() {
// Register settings.
register_setting( 'informatiq_sp_settings', 'informatiq_sp_merchant_id' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_client_id' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_client_secret' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_minimum_margin' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_auto_update_enabled' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_update_frequency' );
// Add settings sections.
add_settings_section(
'informatiq_sp_google_settings',
__( 'Google Merchant Center Settings', 'informatiq-smart-pricing' ),
array( $this, 'render_google_settings_section' ),
'informatiq_sp_settings'
);
add_settings_section(
'informatiq_sp_pricing_settings',
__( 'Pricing Settings', 'informatiq-smart-pricing' ),
array( $this, 'render_pricing_settings_section' ),
'informatiq_sp_settings'
);
add_settings_section(
'informatiq_sp_automation_settings',
__( 'Automation Settings', 'informatiq-smart-pricing' ),
array( $this, 'render_automation_settings_section' ),
'informatiq_sp_settings'
);
// Add settings fields.
add_settings_field(
'informatiq_sp_merchant_id',
__( 'Google Merchant ID', 'informatiq-smart-pricing' ),
array( $this, 'render_merchant_id_field' ),
'informatiq_sp_settings',
'informatiq_sp_google_settings'
);
add_settings_field(
'informatiq_sp_oauth_credentials',
__( 'OAuth 2.0 Credentials', 'informatiq-smart-pricing' ),
array( $this, 'render_oauth_credentials_field' ),
'informatiq_sp_settings',
'informatiq_sp_google_settings'
);
add_settings_field(
'informatiq_sp_authorization',
__( 'Google Authorization', 'informatiq-smart-pricing' ),
array( $this, 'render_authorization_field' ),
'informatiq_sp_settings',
'informatiq_sp_google_settings'
);
add_settings_field(
'informatiq_sp_minimum_margin',
__( 'Minimum Margin (%)', 'informatiq-smart-pricing' ),
array( $this, 'render_minimum_margin_field' ),
'informatiq_sp_settings',
'informatiq_sp_pricing_settings'
);
add_settings_field(
'informatiq_sp_auto_update_enabled',
__( 'Enable Automatic Updates', 'informatiq-smart-pricing' ),
array( $this, 'render_auto_update_field' ),
'informatiq_sp_settings',
'informatiq_sp_automation_settings'
);
add_settings_field(
'informatiq_sp_update_frequency',
__( 'Update Frequency', 'informatiq-smart-pricing' ),
array( $this, 'render_update_frequency_field' ),
'informatiq_sp_settings',
'informatiq_sp_automation_settings'
);
}
/**
* Handle OAuth callback from Google.
*/
public function handle_oauth_callback() {
// Check if this is an OAuth callback.
if ( ! isset( $_GET['informatiq_sp_oauth'] ) || $_GET['informatiq_sp_oauth'] !== 'callback' ) {
return;
}
// Verify state parameter.
$state = isset( $_GET['state'] ) ? sanitize_text_field( $_GET['state'] ) : '';
$saved_state = get_transient( 'informatiq_sp_oauth_state' );
if ( empty( $state ) || $state !== $saved_state ) {
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
__( 'OAuth state mismatch. Please try again.', 'informatiq-smart-pricing' ),
'error'
);
return;
}
delete_transient( 'informatiq_sp_oauth_state' );
// Check for errors.
if ( isset( $_GET['error'] ) ) {
$error = sanitize_text_field( $_GET['error'] );
$error_desc = isset( $_GET['error_description'] ) ? sanitize_text_field( $_GET['error_description'] ) : $error;
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
sprintf( __( 'Authorization failed: %s', 'informatiq-smart-pricing' ), $error_desc ),
'error'
);
return;
}
// Get authorization code.
$code = isset( $_GET['code'] ) ? sanitize_text_field( $_GET['code'] ) : '';
if ( empty( $code ) ) {
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
__( 'No authorization code received.', 'informatiq-smart-pricing' ),
'error'
);
return;
}
// Exchange code for tokens.
$client_id = get_option( 'informatiq_sp_client_id' );
$client_secret = get_option( 'informatiq_sp_client_secret' );
$redirect_uri = $this->get_oauth_redirect_uri();
try {
$tokens = Informatiq_SP_Google_API::exchange_code_for_tokens(
$code,
$client_id,
$client_secret,
$redirect_uri
);
// Save refresh token.
update_option( 'informatiq_sp_refresh_token', $tokens['refresh_token'] );
$this->logger->info( 'Google OAuth authorization successful' );
// Redirect to settings page with success message.
wp_redirect( admin_url( 'admin.php?page=informatiq-smart-pricing&oauth=success' ) );
exit;
} catch ( Exception $e ) {
$this->logger->error( 'OAuth token exchange failed: ' . $e->getMessage() );
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
sprintf( __( 'Token exchange failed: %s', 'informatiq-smart-pricing' ), $e->getMessage() ),
'error'
);
}
}
/**
* Get OAuth redirect URI.
*
* @return string Redirect URI.
*/
private function get_oauth_redirect_uri() {
return admin_url( 'admin.php?page=informatiq-smart-pricing&informatiq_sp_oauth=callback' );
}
/**
* Check if plugin is authorized.
*
* @return bool True if authorized.
*/
private function is_authorized() {
return ! empty( get_option( 'informatiq_sp_refresh_token' ) );
}
/**
* Render admin page.
*/
public function render_admin_page() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
// Check for OAuth success message.
if ( isset( $_GET['oauth'] ) && $_GET['oauth'] === 'success' ) {
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_message',
__( 'Google authorization successful!', 'informatiq-smart-pricing' ),
'success'
);
}
// Check if form was submitted.
if ( isset( $_GET['settings-updated'] ) ) {
// Update cron schedule if frequency changed.
$frequency = get_option( 'informatiq_sp_update_frequency', 'daily' );
Informatiq_SP_Scheduler::reschedule( $frequency );
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_message',
__( 'Settings saved successfully.', 'informatiq-smart-pricing' ),
'success'
);
}
?>
' . esc_html__( 'Configure your Google Merchant Center API credentials using OAuth 2.0.', 'informatiq-smart-pricing' ) . '';
}
/**
* Render pricing settings section description.
*/
public function render_pricing_settings_section() {
echo '' . esc_html__( 'Configure pricing rules and safeguards.', 'informatiq-smart-pricing' ) . '
';
}
/**
* Render automation settings section description.
*/
public function render_automation_settings_section() {
echo '' . esc_html__( 'Configure automated price updates.', 'informatiq-smart-pricing' ) . '
';
}
/**
* Render merchant ID field.
*/
public function render_merchant_id_field() {
$value = get_option( 'informatiq_sp_merchant_id' );
?>
is_authorized();
$has_credentials = ! empty( $client_id ) && ! empty( $client_secret );
if ( $is_authorized ) {
?>
get_oauth_redirect_uri(),
$state
);
?>
%
>
>
>
>
logger->get_todays_logs();
?>
__( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
if ( ! $this->is_authorized() ) {
wp_send_json_error( array( 'message' => __( 'Please authorize with Google first.', 'informatiq-smart-pricing' ) ) );
}
try {
$scheduler = new Informatiq_SP_Scheduler( $this->price_updater );
$results = $scheduler->trigger_manual_update();
wp_send_json_success(
array(
'message' => sprintf(
/* translators: %1$d: updated count, %2$d: processed count */
__( 'Sync completed! Updated %1$d of %2$d products.', 'informatiq-smart-pricing' ),
$results['updated'],
$results['processed']
),
'results' => $results,
)
);
} catch ( Exception $e ) {
wp_send_json_error( array( 'message' => $e->getMessage() ) );
}
}
/**
* Handle test connection AJAX request.
*/
public function handle_test_connection() {
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 ) ) {
wp_send_json_error( array( 'message' => __( 'Please enter your Merchant ID.', 'informatiq-smart-pricing' ) ) );
}
if ( empty( $refresh_token ) ) {
wp_send_json_error( array( 'message' => __( 'Please authorize with Google first.', 'informatiq-smart-pricing' ) ) );
}
try {
$google_api = new Informatiq_SP_Google_API(
$merchant_id,
$client_id,
$client_secret,
$refresh_token,
$this->logger
);
$success = $google_api->test_connection();
if ( $success ) {
wp_send_json_success( array( 'message' => __( 'Connection successful!', 'informatiq-smart-pricing' ) ) );
} else {
wp_send_json_error( array( 'message' => __( 'Connection failed. Check your credentials.', 'informatiq-smart-pricing' ) ) );
}
} catch ( Exception $e ) {
wp_send_json_error( array( 'message' => $e->getMessage() ) );
}
}
/**
* Handle revoke authorization AJAX request.
*/
public function handle_revoke_auth() {
check_ajax_referer( 'informatiq_sp_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
delete_option( 'informatiq_sp_refresh_token' );
$this->logger->info( 'Google OAuth authorization revoked' );
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 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 ) ) );
// Get all WooCommerce in-stock products (no limit - load all).
$wc_products = wc_get_products( array(
'status' => 'publish',
'stock_status' => 'instock',
'limit' => -1,
'return' => 'objects',
'type' => array( 'simple', 'variable' ),
) );
$comparison = array();
foreach ( $wc_products as $product ) {
$sku = $product->get_sku();
if ( empty( $sku ) ) {
continue;
}
$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';
$local_price_incl_tax = null;
if ( $base_price ) {
$local_price_incl_tax = wc_get_price_including_tax( $product, array( 'price' => $base_price ) );
}
// 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 ];
}
// Calculate potential gain and determine if update is beneficial.
$suggested_price = $insight['suggested_price'] ?? null;
$potential_gain = null;
$should_update = false;
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_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,
);
}
// Sort by predicted conversion change (highest first), products without data last.
usort( $comparison, function( $a, $b ) {
// Products without insight data go to the end.
if ( ! $a['has_insight'] && $b['has_insight'] ) {
return 1;
}
if ( $a['has_insight'] && ! $b['has_insight'] ) {
return -1;
}
if ( ! $a['has_insight'] && ! $b['has_insight'] ) {
return 0;
}
// Sort by predicted conversion change (highest first).
$conv_a = $a['predicted_conversions_change'] ?? -999;
$conv_b = $b['predicted_conversions_change'] ?? -999;
return $conv_b <=> $conv_a;
} );
wp_send_json_success( array(
'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 ) {
wp_send_json_error( array( 'message' => $e->getMessage() ) );
}
}
/**
* 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.
*
* @param string $hook Current admin page hook.
*/
public function enqueue_admin_assets( $hook ) {
if ( 'woocommerce_page_informatiq-smart-pricing' !== $hook ) {
return;
}
wp_enqueue_style(
'informatiq-sp-admin',
INFORMATIQ_SP_PLUGIN_URL . 'assets/css/admin.css',
array(),
INFORMATIQ_SP_VERSION
);
wp_enqueue_script(
'informatiq-sp-admin',
INFORMATIQ_SP_PLUGIN_URL . 'assets/js/admin.js',
array( 'jquery' ),
INFORMATIQ_SP_VERSION,
true
);
wp_localize_script(
'informatiq-sp-admin',
'informatiqSP',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'informatiq_sp_admin' ),
'strings' => array(
'syncInProgress' => __( 'Sync in progress...', 'informatiq-smart-pricing' ),
'testInProgress' => __( 'Testing connection...', 'informatiq-smart-pricing' ),
'revokeConfirm' => __( 'Are you sure you want to revoke Google authorization?', 'informatiq-smart-pricing' ),
'revokeInProgress' => __( 'Revoking...', 'informatiq-smart-pricing' ),
),
)
);
}
}