__( 'Dead archive detection', 'ai-eshop-optimizer' ), 'description' => __( 'Archive pages (anchor_term_id) with reco impressions but ZERO cart-adds in the same period — the dead-zone candidates that may need a redesign, a hero, or to be retired.', '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', 'properties' => array( 'period' => array( 'type' => 'string', 'default' => 'last_30_days', 'enum' => PeriodHelper::ALLOWED, ), 'min_impressions' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 1000, 'default' => 25, 'description' => 'Floor for reco impressions on an archive before it is considered dead-but-seen.', ), 'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 25, ), ), ); } public static function execute( $input ) { global $wpdb; $input = is_array( $input ) ? $input : array(); $period = PeriodHelper::resolve( $input['period'] ?? 'last_30_days' ); $min_imp = isset( $input['min_impressions'] ) ? max( 1, (int) $input['min_impressions'] ) : 25; $limit = isset( $input['limit'] ) ? max( 1, min( 100, (int) $input['limit'] ) ) : 25; $reco = $wpdb->prefix . 'aieo_reco_daily'; $addcart = $wpdb->prefix . 'aieo_addcart_events'; $exists = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (%s, %s)", $reco, $addcart ) ); if ( $exists < 2 ) { return array( 'period' => $period, 'rows' => array(), 'note' => __( 'Attribution tables missing.', 'ai-eshop-optimizer' ) ); } $imp_rows = $wpdb->get_results( $wpdb->prepare( "SELECT anchor_term_id AS term_id, anchor_term_tax AS taxonomy, SUM(impressions) AS impressions, SUM(sessions) AS sessions FROM {$reco} WHERE date BETWEEN DATE(%s) AND DATE(%s) AND anchor_term_id > 0 GROUP BY anchor_term_id, anchor_term_tax HAVING impressions >= %d", $period['since'], $period['until'], $min_imp ), ARRAY_A ); if ( empty( $imp_rows ) ) { return array( 'period' => $period, 'rows' => array(), 'note' => __( 'No archive impressions in window.', 'ai-eshop-optimizer' ) ); } $cart_rows = $wpdb->get_results( $wpdb->prepare( "SELECT source_meta, COUNT(*) AS n FROM {$addcart} WHERE ts_added BETWEEN %s AND %s AND source = 'archive' AND source_meta IS NOT NULL GROUP BY source_meta", $period['since'], $period['until'] ), ARRAY_A ); $cart_keys = array(); foreach ( (array) $cart_rows as $r ) { $meta = json_decode( (string) $r['source_meta'], true ); if ( ! is_array( $meta ) ) { continue; } $tid = isset( $meta['archive_term_id'] ) ? (int) $meta['archive_term_id'] : 0; $tx = isset( $meta['archive_term_tax'] ) ? (string) $meta['archive_term_tax'] : ''; if ( $tid > 0 && $tx !== '' ) { $cart_keys[ $tx . ':' . $tid ] = (int) $r['n']; } } $out = array(); foreach ( (array) $imp_rows as $r ) { $tid = (int) $r['term_id']; $tx = (string) $r['taxonomy']; $key = $tx . ':' . $tid; if ( isset( $cart_keys[ $key ] ) && $cart_keys[ $key ] > 0 ) { continue; } $term = get_term( $tid, $tx ); $is_term = $term instanceof \WP_Term; $out[] = array( 'taxonomy' => $tx, 'term_id' => $tid, 'name' => $is_term ? (string) $term->name : '', 'archive_url' => $is_term ? get_term_link( $term ) : '', 'impressions' => (int) $r['impressions'], 'sessions' => (int) $r['sessions'], 'cart_adds' => 0, ); if ( count( $out ) >= $limit ) { break; } } usort( $out, static function ( $a, $b ) { return $b['impressions'] <=> $a['impressions']; } ); return array( 'period' => $period, 'min_impressions' => $min_imp, 'rows' => $out, ); } }