container = $container; } /** * Run the daily SOA check. * * Queries each active AI platform with generic shopping queries * and records whether the store is mentioned. * * @return void */ public function run_daily_check(): void { if ( ! aicommerce_is_business() ) { return; } $query_bank = new QueryBank(); $queries = $query_bank->get_all_queries(); $platforms = $this->get_active_platforms(); $store_url = home_url(); $store_name = get_bloginfo( 'name' ); $competitors = $this->get_competitor_domains(); if ( empty( $platforms ) || empty( $queries ) ) { return; } global $wpdb; foreach ( $platforms as $platform ) { $mentions = 0; $total = 0; foreach ( $queries as $query ) { try { $response = $this->query_platform( $platform, $query ); if ( ! $response ) { continue; } ++$total; $own = $query_bank->detect_own_mention( $response, $store_url, $store_name ); $comps = $query_bank->detect_competitor_mentions( $response, $competitors ); if ( $own ) { ++$mentions; } // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->insert( $wpdb->prefix . 'aicommerce_soa_results', array( 'query_text' => $query, 'platform' => $platform, 'own_store_mentioned' => $own ? 1 : 0, 'competitor_mentions' => wp_json_encode( $comps ), 'response_snippet' => substr( $response, 0, 500 ), 'checked_at' => current_time( 'mysql' ), ), array( '%s', '%s', '%d', '%s', '%s', '%s' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter // Respect rate limits. sleep( 1 ); } catch ( \Exception $e ) { // Log and continue to next query. continue; } } // Update daily aggregate. $soa = $total > 0 ? round( ( $mentions / $total ) * 100, 2 ) : 0; // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->replace( $wpdb->prefix . 'aicommerce_soa_daily', array( 'date' => current_time( 'Y-m-d' ), 'platform' => $platform, 'queries_run' => $total, 'own_mentions' => $mentions, 'share_of_answer' => $soa, ), array( '%s', '%s', '%d', '%d', '%f' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter } update_option( 'aicommerce_soa_last_run', current_time( 'c' ), false ); } /** * Query an AI platform with a shopping question. * * Uses the merchant's own BYOK API keys. * * @param string $platform Platform identifier. * @param string $query Shopping query to ask. * * @return string|null AI response text, or null on failure. */ public function query_platform( string $platform, string $query ): ?string { $prompt = "A user asked: \"{$query}\"\n\nProvide a helpful shopping recommendation response with specific product and store suggestions."; try { switch ( $platform ) { case 'openai': if ( ! $this->container->has( OpenAIProvider::class ) ) { return null; } /** @var OpenAIProvider $provider */ $provider = $this->container->get( OpenAIProvider::class ); return $provider->complete( $prompt, 300 ); case 'anthropic': if ( ! $this->container->has( AnthropicProvider::class ) ) { return null; } /** @var AnthropicProvider $provider */ $provider = $this->container->get( AnthropicProvider::class ); return $provider->complete( $prompt, 300 ); case 'gemini': if ( ! $this->container->has( GeminiProvider::class ) ) { return null; } /** @var GeminiProvider $provider */ $provider = $this->container->get( GeminiProvider::class ); return $provider->complete( $prompt, 300 ); default: return null; } } catch ( \Exception $e ) { return null; } } /** * Get platforms that have API keys configured. * * @return string[] Active platform identifiers. */ public function get_active_platforms(): array { $platforms = array(); if ( get_option( 'aicommerce_openai_api_key' ) ) { $platforms[] = 'openai'; } if ( get_option( 'aicommerce_anthropic_api_key' ) ) { $platforms[] = 'anthropic'; } if ( get_option( 'aicommerce_gemini_api_key' ) ) { $platforms[] = 'gemini'; } return $platforms; } /** * Get SOA summary for the dashboard. * * @param int $days Number of days to look back. * * @return array Summary data. */ public function get_soa_summary( int $days = 30 ): array { global $wpdb; // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $rows = $wpdb->get_results( $wpdb->prepare( "SELECT date, platform, share_of_answer, queries_run, own_mentions FROM {$wpdb->prefix}aicommerce_soa_daily WHERE date >= DATE_SUB(CURDATE(), INTERVAL %d DAY) ORDER BY date ASC", $days ), ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $by_platform = array(); $trend_data = array(); foreach ( ( $rows ?: array() ) as $row ) { $by_platform[ $row['platform'] ][] = $row; $trend_data[] = $row; } $current_soa = array(); foreach ( $by_platform as $platform => $data ) { $latest = end( $data ); $first = reset( $data ); $current_soa[ $platform ] = array( 'current' => (float) $latest['share_of_answer'], 'change' => round( (float) $latest['share_of_answer'] - (float) $first['share_of_answer'], 2 ), ); } $last_run = get_option( 'aicommerce_soa_last_run', '' ); return array( 'by_platform' => $current_soa, 'trend_data' => $trend_data, 'last_run' => $last_run, 'next_run' => $last_run ? gmdate( 'c', strtotime( '+1 day', strtotime( $last_run ) ) ) : null, ); } /** * Get competitor domains from the DB. * * @return string[] */ private function get_competitor_domains(): array { global $wpdb; // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $domains = $wpdb->get_col( "SELECT competitor_domain FROM {$wpdb->prefix}aicommerce_soa_competitors WHERE is_active = 1" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return $domains ?: array(); } }