*/ public const AI_PLATFORMS = [ 'Perplexity' => [ 'perplexity.ai' ], 'ChatGPT' => [ 'chatgpt.com', 'chat.openai.com' ], 'Gemini' => [ 'gemini.google.com', 'bard.google.com' ], 'Copilot' => [ 'copilot.microsoft.com' ], 'Claude' => [ 'claude.ai' ], 'You.com' => [ 'you.com' ], 'Grok' => [ 'grok.com' ], ]; /** * Known AI UTM source values. * * @var string[] */ private const AI_UTM_SOURCES = [ 'chatgpt', 'perplexity', 'gemini', 'copilot', 'claude' ]; /** * WordPress database instance. * * @var \wpdb */ private \wpdb $wpdb; /** * Constructor. * * @param \wpdb $wpdb WordPress database instance. */ public function __construct( \wpdb $wpdb ) { $this->wpdb = $wpdb; } /** * Detect AI referral session from HTTP referer or UTM parameters. * * Hooked to `wp` on the front-end only. * * @return void */ public function detect_ai_session(): void { if ( is_admin() ) { return; } $platform = null; $referrer = ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended $raw_referrer = isset( $_SERVER['HTTP_REFERER'] ) ? wp_unslash( $_SERVER['HTTP_REFERER'] ) : ''; if ( ! empty( $raw_referrer ) ) { $referrer = esc_url_raw( $raw_referrer ); $platform = $this->identify_platform( $referrer ); } if ( null === $platform ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $utm_source = isset( $_GET['utm_source'] ) ? sanitize_key( $_GET['utm_source'] ) : ''; if ( ! empty( $utm_source ) ) { $platform = $this->identify_platform_by_utm( $utm_source ); } } if ( null === $platform ) { return; } if ( headers_sent() ) { return; } $cookie_path = defined( 'COOKIEPATH' ) ? COOKIEPATH : '/'; $cookie_domain = defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : ''; $secure = is_ssl(); $expiry = time() + HOUR_IN_SECONDS; setcookie( 'aicommerce_ai_source', $platform, $expiry, $cookie_path, $cookie_domain, $secure, true ); setcookie( 'aicommerce_ai_referrer', esc_url_raw( $referrer ), $expiry, $cookie_path, $cookie_domain, $secure, true ); } /** * Record an AI-attributed order at checkout. * * Hooked to `woocommerce_thankyou`. * * @param int $order_id WooCommerce order ID. * @return void */ public function record_ai_order( int $order_id ): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $platform = isset( $_COOKIE['aicommerce_ai_source'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['aicommerce_ai_source'] ) ) : ''; if ( empty( $platform ) ) { return; } $order = wc_get_order( $order_id ); if ( ! $order ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $referrer_url = isset( $_COOKIE['aicommerce_ai_referrer'] ) ? sanitize_url( wp_unslash( $_COOKIE['aicommerce_ai_referrer'] ) ) : ''; // 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 $this->wpdb->insert( "{$this->wpdb->prefix}aicommerce_attribution", [ 'order_id' => $order_id, 'referrer_source' => sanitize_text_field( $platform ), 'referrer_url' => $referrer_url, 'revenue' => (float) $order->get_total(), 'currency' => $order->get_currency(), 'attributed_at' => current_time( 'mysql' ), ], [ '%d', '%s', '%s', '%f', '%s', '%s' ] ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter } /** * Get attribution summary for a given number of days. * * @param int $days Number of days to look back (default 30). * @return array{ * total_revenue: float, * order_count: int, * by_platform: array, * daily: array, * vs_previous: array{revenue_change_pct: float, order_change_pct: float}, * } */ public function get_summary( int $days = 30 ): array { $table = "{$this->wpdb->prefix}aicommerce_attribution"; $date_start = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) ); $prev_start = gmdate( 'Y-m-d H:i:s', strtotime( '-' . ( $days * 2 ) . ' days' ) ); // 1. Total revenue and order count for the current period. // 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 $totals = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT COALESCE(SUM(revenue), 0) AS total_revenue, COUNT(*) AS order_count FROM {$table} WHERE attributed_at >= %s", $date_start ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $total_revenue = null !== $totals ? (float) $totals->total_revenue : 0.0; $order_count = null !== $totals ? (int) $totals->order_count : 0; // 2. Revenue and order count grouped by platform. // 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 $platform_rows = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT referrer_source, COALESCE(SUM(revenue), 0) AS revenue, COUNT(*) AS orders FROM {$table} WHERE attributed_at >= %s GROUP BY referrer_source", $date_start ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $by_platform = []; if ( is_array( $platform_rows ) ) { foreach ( $platform_rows as $row ) { $by_platform[ $row->referrer_source ] = [ 'revenue' => (float) $row->revenue, 'orders' => (int) $row->orders, ]; } } // 3. Daily breakdown. // 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 $daily_rows = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT DATE(attributed_at) AS date, COALESCE(SUM(revenue), 0) AS revenue, COUNT(*) AS orders FROM {$table} WHERE attributed_at >= %s GROUP BY DATE(attributed_at) ORDER BY date ASC", $date_start ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $daily = []; if ( is_array( $daily_rows ) ) { foreach ( $daily_rows as $row ) { $daily[] = [ 'date' => $row->date, 'revenue' => (float) $row->revenue, 'orders' => (int) $row->orders, ]; } } // 4. Previous period totals for comparison. // 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 $prev_totals = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT COALESCE(SUM(revenue), 0) AS total_revenue, COUNT(*) AS order_count FROM {$table} WHERE attributed_at >= %s AND attributed_at < %s", $prev_start, $date_start ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $prev_revenue = null !== $prev_totals ? (float) $prev_totals->total_revenue : 0.0; $prev_orders = null !== $prev_totals ? (int) $prev_totals->order_count : 0; $revenue_change_pct = 0.0 < $prev_revenue ? round( ( ( $total_revenue - $prev_revenue ) / $prev_revenue ) * 100, 2 ) : 0.0; $order_change_pct = 0 < $prev_orders ? round( ( ( $order_count - $prev_orders ) / $prev_orders ) * 100, 2 ) : 0.0; return [ 'total_revenue' => $total_revenue, 'order_count' => $order_count, 'by_platform' => $by_platform, 'daily' => $daily, 'vs_previous' => [ 'revenue_change_pct' => $revenue_change_pct, 'order_change_pct' => $order_change_pct, ], ]; } /** * Identify an AI platform from a referrer URL. * * @param string $referrer The HTTP referrer URL. * @return string|null Platform name or null if not recognized. */ private function identify_platform( string $referrer ): ?string { $host = wp_parse_url( $referrer, PHP_URL_HOST ); if ( empty( $host ) ) { return null; } $host = strtolower( $host ); foreach ( self::AI_PLATFORMS as $platform => $domains ) { foreach ( $domains as $domain ) { if ( $host === $domain || str_ends_with( $host, '.' . $domain ) ) { return $platform; } } } return null; } /** * Identify an AI platform from a UTM source parameter. * * @param string $utm_source The sanitized utm_source value. * @return string|null Platform name or null if not recognized. */ private function identify_platform_by_utm( string $utm_source ): ?string { $utm_source = strtolower( $utm_source ); if ( ! in_array( $utm_source, self::AI_UTM_SOURCES, true ) ) { return null; } $utm_to_platform = [ 'chatgpt' => 'ChatGPT', 'perplexity' => 'Perplexity', 'gemini' => 'Gemini', 'copilot' => 'Copilot', 'claude' => 'Claude', ]; return $utm_to_platform[ $utm_source ] ?? null; } }