__( 'Buy probability by engagement', 'ai-eshop-optimizer' ), 'description' => __( 'For one product, return a 16-row bucket table (scroll-depth quartile × time-on-page quartile) with viewers, cart-adders, and empirical P(cart-add) per bucket. Heuristic — pure aggregate, not ML. Use to answer "if a visitor scrolls 75%+ and stays 90s+, how likely are they to buy?".', '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( 'product_id' ), 'properties' => array( 'product_id' => array( 'type' => 'integer', 'minimum' => 1, ), 'period_days' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 730, 'default' => 90, ), ), ); } public static function execute( $input ) { $input = is_array( $input ) ? $input : array(); $product_id = isset( $input['product_id'] ) ? (int) $input['product_id'] : 0; $period_days = isset( $input['period_days'] ) ? (int) $input['period_days'] : 90; if ( $product_id <= 0 ) { return new \WP_Error( 'aieo_invalid_input', __( 'product_id is required.', 'ai-eshop-optimizer' ) ); } if ( ! class_exists( 'AIEO_Conversion_Probability_Model' ) ) { return new \WP_Error( 'aieo_unavailable', __( 'AIEO_Conversion_Probability_Model not loaded.', 'ai-eshop-optimizer' ) ); } $cached = self::from_materialised( $product_id, $period_days ); if ( null !== $cached ) { return $cached; } return \AIEO_Conversion_Probability_Model::compute_bucket_table( $product_id, $period_days ); } private static function from_materialised( $product_id, $period_days ) { global $wpdb; if ( 90 !== (int) $period_days ) { return null; } $t = $wpdb->prefix . 'aieo_conversion_buckets'; if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) !== $t ) { return null; } $rows = $wpdb->get_results( $wpdb->prepare( "SELECT scroll_quartile, time_quartile, viewers, cart_adders, p_cart_add, computed_at FROM `{$t}` WHERE product_id = %d ORDER BY scroll_quartile, time_quartile", $product_id ), ARRAY_A ); if ( ! $rows ) { return null; } $newest = 0; foreach ( $rows as $r ) { $ts = isset( $r['computed_at'] ) ? strtotime( (string) $r['computed_at'] . ' UTC' ) : 0; if ( $ts > $newest ) { $newest = $ts; } } if ( ! $newest || ( time() - $newest ) > ( 14 * DAY_IN_SECONDS ) ) { return null; } $out = array(); foreach ( $rows as $r ) { $out[] = array( 'scroll_bucket' => (int) $r['scroll_quartile'], 'time_bucket' => (int) $r['time_quartile'], 'viewers' => (int) $r['viewers'], 'cart_adders' => (int) $r['cart_adders'], 'p_cart_add' => null === $r['p_cart_add'] ? null : round( (float) $r['p_cart_add'], 4 ), ); } return array( 'product_id' => (int) $product_id, 'period_days' => 90, 'rows' => $out, 'source' => 'materialised', 'computed_at' => gmdate( 'Y-m-d H:i:s', $newest ), ); } }