__( 'PRODUCT FEED', 'agentic-commerce-for-woocommerce' ), 'title' => __( 'Getting listed is easy. Getting chosen is the hard part.', 'agentic-commerce-for-woocommerce' ), 'sub' => __( 'AI assistants compare products on attributes. Anything missing is a reason to skip you. This scores your catalog the way they read it — before a single row is sent anywhere.', 'agentic-commerce-for-woocommerce' ), ) ); $h = self::health(); self::render_health( $h ); self::render_issues( $h ); self::permanence_warning(); Xpay_UI::close(); } /** * Score a sample of the catalog on the fields that decide whether a product * can compete in an AI answer. * * @return array{sampled:int,total:int,issues:array,score:int} */ public static function health() { $out = array( 'sampled' => 0, 'total' => 0, 'issues' => array(), 'score' => 0, ); if ( ! function_exists( 'wc_get_products' ) ) { return $out; } $counts = function_exists( 'wp_count_posts' ) ? wp_count_posts( 'product' ) : null; $out['total'] = ( $counts && isset( $counts->publish ) ) ? (int) $counts->publish : 0; $products = wc_get_products( array( 'limit' => self::SAMPLE, 'status' => 'publish', 'return' => 'objects', ) ); if ( ! is_array( $products ) || ! $products ) { return $out; } $defs = array( 'description' => array( __( 'Missing description', 'agentic-commerce-for-woocommerce' ), __( 'An empty description is rejected as a misformatted column, and the assistant has nothing to compare on.', 'agentic-commerce-for-woocommerce' ), 'high' ), 'image' => array( __( 'No product image', 'agentic-commerce-for-woocommerce' ), __( 'Products without an image are routinely skipped in shopping answers.', 'agentic-commerce-for-woocommerce' ), 'high' ), 'price' => array( __( 'No usable price', 'agentic-commerce-for-woocommerce' ), __( 'A blank or zero price is read as unknown, not as free. Quote-priced products cannot be listed.', 'agentic-commerce-for-woocommerce' ), 'high' ), 'gtin' => array( __( 'No brand or product identifier', 'agentic-commerce-for-woocommerce' ), __( 'Identifiers are how an assistant knows two listings are the same item. Without one you compete poorly on price comparisons.', 'agentic-commerce-for-woocommerce' ), 'med' ), 'short' => array( __( 'Very short description', 'agentic-commerce-for-woocommerce' ), __( 'Under about 100 characters rarely carries the attributes a shopper asks about.', 'agentic-commerce-for-woocommerce' ), 'med' ), ); $fail = array(); foreach ( $products as $p ) { if ( ! is_a( $p, 'WC_Product' ) ) { continue; } $out['sampled']++; $desc = trim( wp_strip_all_tags( (string) $p->get_description() ) ); if ( '' === $desc ) { $fail['description'] = ( isset( $fail['description'] ) ? $fail['description'] : 0 ) + 1; } elseif ( ( function_exists( 'mb_strlen' ) ? mb_strlen( $desc ) : strlen( $desc ) ) < 100 ) { $fail['short'] = ( isset( $fail['short'] ) ? $fail['short'] : 0 ) + 1; } if ( ! $p->get_image_id() ) { $fail['image'] = ( isset( $fail['image'] ) ? $fail['image'] : 0 ) + 1; } // ⛔ price 0 means UNKNOWN, never free. Both are unlistable, and they // are counted the same way here for that reason. $price = $p->get_price(); if ( '' === $price || null === $price || 0.0 === (float) $price ) { $fail['price'] = ( isset( $fail['price'] ) ? $fail['price'] : 0 ) + 1; } $gtin = ''; foreach ( array( '_wpm_gtin_code', '_gtin', 'gtin', '_global_unique_id' ) as $key ) { $v = $p->get_meta( $key ); if ( $v ) { $gtin = (string) $v; break; } } if ( '' === $gtin && '' === (string) $p->get_sku() ) { $fail['gtin'] = ( isset( $fail['gtin'] ) ? $fail['gtin'] : 0 ) + 1; } } foreach ( $fail as $key => $n ) { $out['issues'][ $key ] = array( 'n' => $n, 'label' => $defs[ $key ][0], 'detail' => $defs[ $key ][1], 'sev' => $defs[ $key ][2], ); } uasort( $out['issues'], static function ( $a, $b ) { return $b['n'] <=> $a['n']; } ); // Score weights blocking issues twice as heavily as advisory ones. $weighted = 0; foreach ( $out['issues'] as $i ) { $weighted += ( 'high' === $i['sev'] ) ? $i['n'] * 2 : $i['n']; } $worst = max( 1, $out['sampled'] * 2 ); $out['score'] = (int) max( 0, min( 100, round( 100 - ( $weighted / $worst ) * 100 ) ) ); return $out; } /** * The score ring. * * @param array $h Health. * @return void */ private static function render_health( $h ) { if ( ! $h['sampled'] ) { echo '
'; Xpay_UI::empty_state( __( 'No published products to score', 'agentic-commerce-for-woocommerce' ), __( 'Add products and publish them, and this fills in automatically.', 'agentic-commerce-for-woocommerce' ) ); echo '
'; return; } Xpay_UI::section( __( 'Catalog health', 'agentic-commerce-for-woocommerce' ), sprintf( /* translators: 1: sampled count, 2: total published. */ __( 'Scored from your %1$s most recently added products, of %2$s published. Nothing has been sent anywhere.', 'agentic-commerce-for-woocommerce' ), number_format_i18n( $h['sampled'] ), number_format_i18n( $h['total'] ) ) ); echo '
'; Xpay_UI::ring( $h['score'], __( 'Catalog health score', 'agentic-commerce-for-woocommerce' ) ); echo '

' . esc_html__( 'This is a sample, not an audit of every product. It is meant to tell you whether you have a systemic gap worth fixing, not to grade individual items.', 'agentic-commerce-for-woocommerce' ) . '

'; } /** * Issue list. * * @param array $h Health. * @return void */ private static function render_issues( $h ) { if ( ! $h['sampled'] ) { return; } Xpay_UI::section( __( 'What to fix first', 'agentic-commerce-for-woocommerce' ) ); if ( ! $h['issues'] ) { echo '
'; Xpay_UI::empty_state( __( 'Nothing blocking found', 'agentic-commerce-for-woocommerce' ), __( 'Every sampled product had a description, an image, a usable price and an identifier.', 'agentic-commerce-for-woocommerce' ) ); echo '
'; return; } echo '
'; foreach ( $h['issues'] as $i ) { Xpay_UI::check( $i['sev'], sprintf( /* translators: 1: issue label, 2: affected count, 3: sampled count. */ __( '%1$s — %2$s of %3$s sampled', 'agentic-commerce-for-woocommerce' ), $i['label'], number_format_i18n( $i['n'] ), number_format_i18n( $h['sampled'] ) ), $i['detail'], admin_url( 'edit.php?post_type=product' ), __( 'Products', 'agentic-commerce-for-woocommerce' ) ); } echo '
'; } /** * The warning that must be read before a first delivery ever happens. * * @return void */ private static function permanence_warning() { Xpay_UI::section( __( 'Before you send a feed anywhere', 'agentic-commerce-for-woocommerce' ) ); Xpay_UI::note( 'warn', '' . esc_html__( 'Anything you send to a ChatGPT Ads feed is permanent.', 'agentic-commerce-for-woocommerce' ) . ' ' . esc_html__( 'The platform adds products but never removes them. Taking a product out of a later upload does not delete it, and deleting the feed itself does not delete the products either — they stay in your advertising account with no way to reach them. This is why we score your catalog here first, and why testing with "just a few products" is the one thing you should not do.', 'agentic-commerce-for-woocommerce' ) ); Xpay_UI::note( 'info', esc_html__( 'A ChatGPT Ads feed is for advertising only. Appearing in ordinary ChatGPT shopping answers comes through a separate route and is not affected by anything on this screen.', 'agentic-commerce-for-woocommerce' ) ); } }