__( 'Recommend products for customer', 'ai-eshop-optimizer' ), 'description' => __( "Return a ranked list of product recommendations for one customer. Leverages the customer's wishlisted/affinity brands and the cached bundle recommendations (`wp_aieo_product_recommendations`). PII-safe.", 'ai-eshop-optimizer' ), 'category' => \AIEO\Claude\Bootstrap::CATEGORY, 'input_schema' => self::input_schema(), 'output_schema' => array( 'type' => 'object' ), 'execute_callback' => array( __CLASS__, 'execute' ), 'permission_callback' => array( __CLASS__, 'permission_check' ), 'meta' => array( 'annotations' => array( 'readonly' => true, 'idempotent' => true, ), 'show_in_rest' => true, ), ) ); } public static function permission_check( $input = null ) { unset( $input ); return current_user_can( 'manage_woocommerce' ); } private static function input_schema() { return array( 'default' => array(), 'type' => 'object', 'required' => array( 'customer_id' ), 'properties' => array( 'customer_id' => array( 'type' => 'integer', 'minimum' => 1, ), 'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 50, 'default' => 12, ), ), ); } public static function execute( $input ) { $input = is_array( $input ) ? $input : array(); $user_id = isset( $input['customer_id'] ) ? (int) $input['customer_id'] : 0; $limit = isset( $input['limit'] ) ? max( 1, min( 50, (int) $input['limit'] ) ) : 12; if ( $user_id <= 0 ) { return new \WP_Error( 'aieo_invalid_input', __( 'customer_id is required.', 'ai-eshop-optimizer' ) ); } $seed_brand_ids = array(); if ( class_exists( 'AIEO_Customer_Intelligence_Query' ) ) { $seed_brand_ids = \AIEO_Customer_Intelligence_Query::get_brand_preferences( $user_id, 5 ); } $seed_product_ids = self::recent_rated_products( $user_id, 5 ); $accum = array(); if ( ! empty( $seed_product_ids ) && class_exists( 'AIEO_Recommendations_Cache' ) ) { $rels = \AIEO_Recommendations_Cache::get_valid_relation_types(); $rels = array_intersect( $rels, array( 'related', 'upsells', 'crosssells', 'product_needs' ) ); foreach ( $seed_product_ids as $sid ) { foreach ( $rels as $rel ) { $cached = \AIEO_Recommendations_Cache::get( $sid, $rel ); if ( ! is_array( $cached ) || empty( $cached ) ) { continue; } foreach ( $cached as $row ) { $pid = isset( $row['product_id'] ) ? (int) $row['product_id'] : ( isset( $row['id'] ) ? (int) $row['id'] : 0 ); if ( $pid <= 0 || $pid === $sid ) { continue; } $accum[ $pid ] = ( $accum[ $pid ] ?? 0 ) + 1; } } } } if ( empty( $accum ) && ! empty( $seed_brand_ids ) ) { global $wpdb; $placeholders = implode( ',', array_fill( 0, count( $seed_brand_ids ), '%d' ) ); $pids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT tr.object_id FROM {$wpdb->term_relationships} tr INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tt.taxonomy = 'product_brand' AND tt.term_id IN ({$placeholders}) LIMIT %d", array_merge( $seed_brand_ids, array( $limit * 3 ) ) ) ); foreach ( (array) $pids as $pid ) { $accum[ (int) $pid ] = 1; } } arsort( $accum, SORT_NUMERIC ); $recommended = array(); $i = 0; foreach ( $accum as $pid => $score ) { $recommended[] = array( 'product_id' => (int) $pid, 'title' => get_the_title( (int) $pid ), 'permalink' => get_permalink( (int) $pid ), 'signal' => (int) $score, ); if ( ++$i >= $limit ) { break; } } return array( 'customer_id' => $user_id, 'pseudonym' => 'Customer #' . $user_id, 'seed_brand_ids' => $seed_brand_ids, 'seed_product_ids' => $seed_product_ids, 'recommendations' => $recommended, ); } private static function recent_rated_products( $user_id, $limit = 5 ) { global $wpdb; $table = $wpdb->prefix . 'aieo_ratings'; $exists = (string) $wpdb->get_var( $wpdb->prepare( "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s LIMIT 1", $table ) ); if ( $exists === '' ) { return array(); } $pids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT product_id FROM {$table} WHERE user_id = %d AND rating > 0 AND (object_type = 'product' OR object_type IS NULL) ORDER BY created_at DESC LIMIT %d", $user_id, max( 1, (int) $limit ) ) ); return array_values( array_filter( array_map( 'intval', (array) $pids ), static function ( $v ) { return $v > 0; } ) ); } }