logger = $logger; } /** * Process all in-stock products. * * @return array Results summary. */ public function process_all_products() { $results = array( 'processed' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => 0, ); // Get plugin settings. $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' ); $minimum_margin = (float) get_option( 'informatiq_sp_minimum_margin', 10 ); // Validate settings. if ( empty( $merchant_id ) || empty( $refresh_token ) ) { $this->logger->error( 'Google Merchant settings not configured. Please configure and authorize first.' ); return $results; } // Initialize Google API. try { $google_api = new Informatiq_SP_Google_API( $merchant_id, $client_id, $client_secret, $refresh_token, $this->logger ); } catch ( Exception $e ) { $this->logger->error( 'Failed to initialize Google API: ' . $e->getMessage() ); return $results; } // Get all in-stock products. $products = $this->get_instock_products(); $this->logger->info( sprintf( 'Starting price update for %d in-stock products', count( $products ) ) ); foreach ( $products as $product ) { $results['processed']++; try { $updated = $this->process_single_product( $product, $google_api, $minimum_margin ); if ( $updated ) { $results['updated']++; } else { $results['skipped']++; } } catch ( Exception $e ) { $results['errors']++; $this->logger->error( sprintf( 'Error processing product #%d: %s', $product->get_id(), $e->getMessage() ) ); } } $this->logger->info( sprintf( 'Price update completed. Processed: %d, Updated: %d, Skipped: %d, Errors: %d', $results['processed'], $results['updated'], $results['skipped'], $results['errors'] ) ); return $results; } /** * Get all in-stock products. * * @return array Array of WC_Product objects. */ private function get_instock_products() { $args = array( 'status' => 'publish', 'stock_status' => 'instock', 'limit' => -1, 'return' => 'objects', 'type' => array( 'simple', 'variable' ), ); return wc_get_products( $args ); } /** * Process a single product. * * @param WC_Product $product Product object. * @param Informatiq_SP_Google_API $google_api Google API instance. * @param float $minimum_margin Minimum margin percentage. * @return bool True if price was updated. */ private function process_single_product( $product, $google_api, $minimum_margin ) { // Get product identifiers. $sku = $product->get_sku(); $gtin = get_post_meta( $product->get_id(), '_gtin', true ); if ( empty( $sku ) ) { $this->logger->warning( sprintf( 'Product #%d has no SKU, skipping', $product->get_id() ) ); return false; } // Get competitor price from Google. $competitor_price = $google_api->get_competitive_price( $sku, $gtin ); if ( is_null( $competitor_price ) || $competitor_price <= 0 ) { $this->logger->info( sprintf( 'No competitor price found for product #%d (SKU: %s)', $product->get_id(), $sku ) ); return false; } // Calculate new price. $new_price = $this->calculate_new_price( $product, $competitor_price, $minimum_margin ); if ( is_null( $new_price ) ) { return false; } // Update product price. return $this->update_product_price( $product, $new_price, $competitor_price ); } /** * Calculate new price based on competitor price and minimum margin. * * @param WC_Product $product Product object. * @param float $competitor_price Competitor price (tax-inclusive). * @param float $minimum_margin Minimum margin percentage. * @return float|null New price or null if cannot be calculated. */ private function calculate_new_price( $product, $competitor_price, $minimum_margin ) { // Generate random offset. $random_offset = $this->get_random_offset(); // Calculate target price (tax-inclusive). $target_price_with_tax = $competitor_price - $random_offset; // Get minimum allowed price based on cost and margin. $minimum_price = $this->get_minimum_price( $product, $minimum_margin ); // Determine if we need to handle taxes. $prices_include_tax = wc_prices_include_tax(); if ( $prices_include_tax ) { // Store prices include tax, so we can set the sale price directly. $new_price = $target_price_with_tax; // Check against minimum price. if ( $new_price < $minimum_price ) { $this->logger->warning( sprintf( 'Product #%d: Calculated price %s is below minimum margin %s, using minimum price', $product->get_id(), wc_price( $new_price ), wc_price( $minimum_price ) ) ); $new_price = $minimum_price; } } else { // Store prices exclude tax, need to reverse-calculate base price. $new_price = $this->calculate_price_excluding_tax( $product, $target_price_with_tax ); // Check against minimum price (also excluding tax). $minimum_price_excl_tax = $this->calculate_price_excluding_tax( $product, $minimum_price ); if ( $new_price < $minimum_price_excl_tax ) { $this->logger->warning( sprintf( 'Product #%d: Calculated price %s is below minimum margin %s, using minimum price', $product->get_id(), wc_price( $new_price ), wc_price( $minimum_price_excl_tax ) ) ); $new_price = $minimum_price_excl_tax; } } return $new_price; } /** * Calculate price excluding tax from tax-inclusive price. * * @param WC_Product $product Product object. * @param float $price_with_tax Price including tax. * @return float Price excluding tax. */ private function calculate_price_excluding_tax( $product, $price_with_tax ) { // Get tax rates for the product. $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); if ( empty( $tax_rates ) ) { // No tax, return as-is. return $price_with_tax; } // Calculate total tax rate. $tax_rate = 0; foreach ( $tax_rates as $rate ) { $tax_rate += (float) $rate['rate']; } // Calculate price excluding tax. $price_excl_tax = $price_with_tax / ( 1 + ( $tax_rate / 100 ) ); return round( $price_excl_tax, wc_get_price_decimals() ); } /** * Get minimum allowed price based on cost and margin. * * @param WC_Product $product Product object. * @param float $minimum_margin Minimum margin percentage. * @return float Minimum price. */ private function get_minimum_price( $product, $minimum_margin ) { // Get product cost from WooCommerce Cost of Goods. $cost = get_post_meta( $product->get_id(), '_wc_cog_cost', true ); // Fallback to regular price if cost is not set. if ( empty( $cost ) || $cost <= 0 ) { $cost = $product->get_regular_price(); $this->logger->info( sprintf( 'Product #%d has no cost set, using regular price %s as cost', $product->get_id(), wc_price( $cost ) ) ); } // Calculate minimum price with margin. $minimum_price = $cost * ( 1 + ( $minimum_margin / 100 ) ); return $minimum_price; } /** * Get random offset between min and max. * * @return float Random offset. */ private function get_random_offset() { return $this->min_offset + ( mt_rand() / mt_getrandmax() ) * ( $this->max_offset - $this->min_offset ); } /** * Update product price. * * @param WC_Product $product Product object. * @param float $new_price New sale price. * @param float $competitor_price Competitor price. * @return bool True if updated successfully. */ private function update_product_price( $product, $new_price, $competitor_price ) { $old_price = $product->get_sale_price(); // Only update if price has changed. if ( (float) $old_price === (float) $new_price ) { $this->logger->info( sprintf( 'Product #%d price unchanged at %s', $product->get_id(), wc_price( $new_price ) ) ); return false; } // Set sale price. $product->set_sale_price( $new_price ); $product->save(); // Log price update. $this->logger->log_price_update( $product->get_id(), $product->get_name(), $old_price ? $old_price : $product->get_regular_price(), $new_price, $competitor_price, 'Price updated via automated sync' ); return true; } /** * Process single product by ID (for manual sync). * * @param int $product_id Product ID. * @return array Result with success/error message. */ public function process_product_by_id( $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { return array( 'success' => false, 'message' => 'Product not found', ); } if ( $product->get_stock_status() !== 'instock' ) { return array( 'success' => false, 'message' => 'Product is not in stock', ); } // Get settings. $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' ); $minimum_margin = (float) get_option( 'informatiq_sp_minimum_margin', 10 ); if ( empty( $merchant_id ) || empty( $refresh_token ) ) { return array( 'success' => false, 'message' => 'Google Merchant settings not configured or not authorized', ); } try { $google_api = new Informatiq_SP_Google_API( $merchant_id, $client_id, $client_secret, $refresh_token, $this->logger ); $updated = $this->process_single_product( $product, $google_api, $minimum_margin ); return array( 'success' => true, 'updated' => $updated, 'message' => $updated ? 'Price updated successfully' : 'No update needed', ); } catch ( Exception $e ) { return array( 'success' => false, 'message' => 'Error: ' . $e->getMessage(), ); } } }