Plugin Directory

Changeset 3732072


Ignore:
Timestamp:
10/07/2026 08:06:32 AM (2 weeks ago)
Author:
hxsmyxh
Message:

Release 1.1.0: Google Shopping Custom Labels 0-4, variant item_group_id grouping, smart size/material/pattern heuristics, and live diagnostic product inspector

Location:
world-inspire-product-feed-for-google-shopping
Files:
4 edited
1 copied

Legend:

Unmodified
Added
Removed
  • world-inspire-product-feed-for-google-shopping/tags/1.1.0/readme.txt

    r3730435 r3732072  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.0
     7Stable tag: 1.1.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2121* **Multi-Engine eCommerce Compatibility**: Native auto-detection and data extraction for both **SureCart** (`sc_product`) and **WooCommerce** (`product`). Zero fatal errors if either or neither is active.
    2222* **Google Merchant Center Specification**: Built according to official Google RSS 2.0 standards with all essential namespace attributes: `<g:id>`, `<g:title>`, `<g:description>`, `<g:link>`, `<g:image_link>`, `<g:availability>`, `<g:price>`, `<g:brand>`, `<g:condition>`, `<g:identifier_exists>`, `<g:google_product_category>`, `<g:color>`, `<g:age_group>`, and `<g:gender>`.
     23* **Custom Labels 0–4 for Google Ads / PMax**: Full support for `<g:custom_label_0>` through `<g:custom_label_4>` with rules for price tiers (< $25, $25-$50, $50-$100, $100+), inventory status, category mapping, clearance/promotion tag keywords, and custom post meta fields.
     24* **Variant Grouping (`<g:item_group_id>`)**: Automatically expands WooCommerce variable products (`product_variation`) into individual variant items linked by parent `<g:item_group_id>`, enabling swatch selection in Google Shopping results.
     25* **Variant Attribute Optimization**: Smart extraction for `<g:size>`, `<g:material>` (Gold, Silver, Leather, Cotton, Gemstones, etc.), and `<g:pattern>` (Solid, Striped, Floral, Beaded) directly from attributes or intelligent NLP heuristics.
    2326* **Multi-Country Shipping Generator**: Automatically embeds multi-destination `<g:shipping>` nodes (defaulting to 8 core global markets: US, CA, UK, JP, SG, HK, AE, MX with custom shipping service names and rates).
    24 * **Intelligent Color & Demographic Extraction**: Automatically maps jewelry, apparel, gemstone, and accessory titles/descriptions into recognized Google taxonomy colors (over 35 shades) and adult/unisex demographics.
     27* **Live Diagnostics & Product Inspector Simulator**: Test and audit any product ID directly in WordPress Admin to verify extracted attributes, custom labels matching reasons, and preview raw `<item>` XML snippets.
    2528* **Automatic Pet Category Mapping**: Heuristically detects pet collars, harnesses, and animal accessories and maps them to `Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses`.
    2629* **Trustmate Review Catalog Sync**: Dedicated `<products>` XML feed format ready for instant connection in your Trustmate merchant dashboard.
    … …  
    3942
    4043= Does this plugin work with SureCart? =
    41 Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, and product galleries.
     44Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, collections, tags, and product galleries.
    4245
    4346= Does this plugin work with WooCommerce? =
    44 Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products.
     47Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products and product variations with `<g:item_group_id>`.
     48
     49= How do Custom Labels (0-4) work? =
     50Navigate to **Settings > Product Feeds > Google Shopping Rules & Custom Labels**. You can configure rules for each slot: Price Range Tiers, Inventory Status, Product Category, Tag/Keyword Matching (e.g. `clearance:Clearance, sale:On Sale, bestseller:Best Seller`), or Custom Post Meta fields.
    4551
    4652= Why does this plugin use extension-free REST URLs? =
    … …  
    5359
    54601. Feed URLs & Health Dashboard: Dynamic REST API endpoints and real-time catalog status.
    55 2. Google Shopping Rules: Global fallback brand defaults, product taxonomies, and NLP color detection.
     612. Google Shopping Rules & Custom Labels: Global fallback brand defaults, product taxonomies, NLP color/size/material detection, and custom labels 0-4.
    56623. Multi-Country Shipping: International destination targeting and custom shipping rates.
    57 4. Live Diagnostics: Catalog extraction verification and Google Merchant Center spec compliance audit.
     634. Live Diagnostics & Product Inspector: Catalog extraction audit, custom label rule matching traces, and raw XML simulator.
    5864
    5965== Changelog ==
     66
     67= 1.1.0 =
     68* Added Google Shopping Custom Labels (custom_label_0 to custom_label_4) with configurable rules: Price Tiers, Inventory Status, Category Mapping, Promotional Tag Keywords, and Custom Post Meta.
     69* Added WooCommerce variable product expansion with `<g:item_group_id>` linking parent and variant products.
     70* Added smart attribute extraction for `<g:size>`, `<g:material>`, and `<g:pattern>` with jewelry/apparel heuristic engines.
     71* Added Live Product Inspector & XML Simulator to Diagnostics tab with rule matching explanation traces.
     72* Enforced Google Shopping 100-character maximum length limit and XML entity escaping on all custom labels.
     73* Added 87 zero-dependency unit and mock E2E integration tests passing 100% green.
    6074
    6175= 1.0.0 =
  • world-inspire-product-feed-for-google-shopping/tags/1.1.0/world-inspire-product-feed-for-google-shopping.php

    r3730219 r3732072  
    33 * Plugin Name: World Inspire Product Feed for Google Shopping
    44 * Plugin URI: https://wordpress.org/plugins/world-inspire-product-feed-for-google-shopping/
    5  * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color and category detection, and anti-cache streaming architecture.
    6  * Version: 1.0.0
     5 * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color/size/material detection, custom labels (0-4), variant item_group_id grouping, and anti-cache streaming architecture.
     6 * Version: 1.1.0
    77 * Author: World Inspire LLC, Vec
    88 * Author URI: https://worldinspirelab.com
    … …  
    1818}
    1919
    20 define( 'WIPF_VERSION', '1.0.0' );
     20define( 'WIPF_VERSION', '1.1.0' );
    2121define( 'WIPF_FILE', __FILE__ );
    2222define( 'WIPF_PATH', plugin_dir_path( __FILE__ ) );
    … …  
    3131function wipf_get_default_settings() {
    3232        return array(
    33                 'brand_name'             => get_bloginfo( 'name' ),
    34                 'default_category'       => 'Apparel & Accessories > Jewelry',
    35                 'default_age_group'      => 'adult',
    36                 'default_gender'         => 'unisex',
    37                 'default_condition'      => 'new',
    38                 'identifier_exists'      => 'no',
    39                 'auto_detect_color'      => 'yes',
    40                 'shipping_countries'     => 'US,CA,GB,JP,SG,HK,AE,MX',
    41                 'shipping_service_name'  => 'Standard Free Shipping',
    42                 'shipping_cost'          => '0.00',
    43                 'shipping_currency'      => 'USD',
    44                 'enable_google_feed'     => 'yes',
    45                 'enable_trustmate_feed'  => 'yes',
    46                 'items_per_feed'         => 500,
     33                'brand_name'              => get_bloginfo( 'name' ),
     34                'default_category'        => 'Apparel & Accessories > Jewelry',
     35                'default_age_group'       => 'adult',
     36                'default_gender'          => 'unisex',
     37                'default_condition'       => 'new',
     38                'identifier_exists'       => 'no',
     39                'auto_detect_color'       => 'yes',
     40                'auto_detect_material'    => 'yes',
     41                'auto_detect_size'        => 'yes',
     42                'auto_detect_pattern'     => 'yes',
     43                'shipping_countries'      => 'US,CA,GB,JP,SG,HK,AE,MX',
     44                'shipping_service_name'   => 'Standard Free Shipping',
     45                'shipping_cost'           => '0.00',
     46                'shipping_currency'       => 'USD',
     47                'enable_google_feed'      => 'yes',
     48                'enable_trustmate_feed'   => 'yes',
     49                'items_per_feed'          => 500,
     50                'enable_custom_labels'    => 'yes',
     51                'custom_label_0_type'     => 'price_tier',
     52                'custom_label_0_value'    => '25,50,100',
     53                'custom_label_0_fallback' => 'Standard',
     54                'custom_label_1_type'     => 'inventory',
     55                'custom_label_1_value'    => '',
     56                'custom_label_1_fallback' => 'Out of Stock',
     57                'custom_label_2_type'     => 'category',
     58                'custom_label_2_value'    => '',
     59                'custom_label_2_fallback' => 'Jewelry',
     60                'custom_label_3_type'     => 'tag_keywords',
     61                'custom_label_3_value'    => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller, new:New Arrival',
     62                'custom_label_3_fallback' => 'Regular',
     63                'custom_label_4_type'     => 'static',
     64                'custom_label_4_value'    => 'World Inspire Catalog',
     65                'custom_label_4_fallback' => '',
    4766        );
    4867}
    … …  
    7897        // Priority compound phrases checked first
    7998        $compounds = array(
    80                 'rose gold'  => 'Rose Gold',
    81                 'tiger eye'  => 'Brown',
     99                'rose gold'    => 'Rose Gold',
     100                'tiger eye'    => 'Brown',
    82101                'lapis lazuli' => 'Blue',
    83102        );
    … …  
    154173
    155174/**
     175 * Intelligent material extraction heuristic.
     176 *
     177 * @param string $title Product title.
     178 * @param string $desc  Product description.
     179 * @return string Extracted material name or empty.
     180 */
     181function wipf_detect_material( $title, $desc = '' ) {
     182        $text = strtolower( $title . ' ' . $desc );
     183
     184        // Priority compound phrases checked first
     185        $compounds = array(
     186                'sterling silver' => 'Sterling Silver',
     187                '925 silver'      => 'Sterling Silver',
     188                'rose gold'       => 'Rose Gold',
     189                'white gold'      => 'White Gold',
     190                'yellow gold'     => 'Yellow Gold',
     191                '14k gold'        => '14K Gold',
     192                '18k gold'        => '18K Gold',
     193                '24k gold'        => '24K Gold',
     194                'stainless steel' => 'Stainless Steel',
     195                'genuine leather' => 'Leather',
     196                'faux leather'    => 'Faux Leather',
     197                'vegan leather'   => 'Faux Leather',
     198                'rose quartz'     => 'Rose Quartz',
     199                'lapis lazuli'    => 'Lapis Lazuli',
     200                'cubic zirconia'  => 'Cubic Zirconia',
     201        );
     202
     203        $detected = array();
     204        foreach ( $compounds as $phrase => $mat_name ) {
     205                if ( false !== strpos( $text, $phrase ) ) {
     206                        if ( ! in_array( $mat_name, $detected, true ) ) {
     207                                $detected[] = $mat_name;
     208                        }
     209                        $text = str_replace( $phrase, '', $text );
     210                }
     211        }
     212
     213        $single_map = array(
     214                'gold'       => 'Gold',
     215                'silver'     => 'Silver',
     216                'platinum'   => 'Platinum',
     217                'titanium'   => 'Titanium',
     218                'brass'      => 'Brass',
     219                'copper'     => 'Copper',
     220                'bronze'     => 'Bronze',
     221                'steel'      => 'Steel',
     222                'diamond'    => 'Diamond',
     223                'pearl'      => 'Pearl',
     224                'amethyst'   => 'Amethyst',
     225                'quartz'     => 'Quartz',
     226                'emerald'    => 'Emerald',
     227                'ruby'       => 'Ruby',
     228                'sapphire'   => 'Sapphire',
     229                'jade'       => 'Jade',
     230                'turquoise'  => 'Turquoise',
     231                'obsidian'   => 'Obsidian',
     232                'onyx'       => 'Onyx',
     233                'opal'       => 'Opal',
     234                'moonstone'  => 'Moonstone',
     235                'topaz'      => 'Topaz',
     236                'garnet'     => 'Garnet',
     237                'tourmaline' => 'Tourmaline',
     238                'amber'      => 'Amber',
     239                'carnelian'  => 'Carnelian',
     240                'citrine'    => 'Citrine',
     241                'crystal'    => 'Crystal',
     242                'leather'    => 'Leather',
     243                'silk'       => 'Silk',
     244                'velvet'     => 'Velvet',
     245                'cotton'     => 'Cotton',
     246                'linen'      => 'Linen',
     247                'wool'       => 'Wool',
     248                'cashmere'   => 'Cashmere',
     249                'satin'      => 'Satin',
     250                'polyester'  => 'Polyester',
     251                'canvas'     => 'Canvas',
     252                'denim'      => 'Denim',
     253                'ceramic'    => 'Ceramic',
     254                'glass'      => 'Glass',
     255                'wood'       => 'Wood',
     256                'silicone'   => 'Silicone',
     257                'resin'      => 'Resin',
     258        );
     259
     260        foreach ( $single_map as $needle => $mat_name ) {
     261                if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) {
     262                        if ( ! in_array( $mat_name, $detected, true ) ) {
     263                                $detected[] = $mat_name;
     264                        }
     265                }
     266        }
     267
     268        if ( count( $detected ) > 2 ) {
     269                return 'Mixed';
     270        } elseif ( ! empty( $detected ) ) {
     271                return implode( '/', $detected );
     272        }
     273
     274        return '';
     275}
     276
     277/**
     278 * Intelligent pattern extraction heuristic.
     279 *
     280 * @param string $title Product title.
     281 * @param string $desc  Product description.
     282 * @return string Extracted pattern or empty.
     283 */
     284function wipf_detect_pattern( $title, $desc = '' ) {
     285        $text = strtolower( $title . ' ' . $desc );
     286
     287        $patterns = array(
     288                'polka dot'    => 'Polka Dot',
     289                'polka dots'   => 'Polka Dot',
     290                'animal print' => 'Animal Print',
     291                'striped'      => 'Striped',
     292                'stripes'      => 'Striped',
     293                'stripe'       => 'Striped',
     294                'floral'       => 'Floral',
     295                'flower'       => 'Floral',
     296                'geometric'    => 'Geometric',
     297                'beaded'       => 'Beaded',
     298                'braided'      => 'Braided',
     299                'plaid'        => 'Plaid',
     300                'checkered'    => 'Checkered',
     301                'leopard'      => 'Animal Print',
     302                'cheetah'      => 'Animal Print',
     303                'zebra'        => 'Animal Print',
     304                'camouflage'   => 'Camouflage',
     305                'camo'         => 'Camouflage',
     306                'marble'       => 'Marble',
     307                'gradient'     => 'Gradient',
     308                'solid'        => 'Solid',
     309        );
     310
     311        foreach ( $patterns as $needle => $pat_name ) {
     312                if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) {
     313                        return $pat_name;
     314                }
     315        }
     316
     317        return '';
     318}
     319
     320/**
     321 * Intelligent size extraction heuristic.
     322 *
     323 * @param string $title Product title.
     324 * @param string $desc  Product description.
     325 * @return string Extracted size or empty.
     326 */
     327function wipf_detect_size( $title, $desc = '' ) {
     328        $text = $title . ' ' . $desc;
     329
     330        // 1. One Size / OS
     331        if ( preg_match( '/\b(one\s*size|osfm|osfa)\b/i', $text ) ) {
     332                return 'One Size';
     333        }
     334
     335        // 2. Ring or shoe sizes: "Size 7", "Size 7.5", "US 8"
     336        if ( preg_match( '/\b(?:size|us|eu)\s*([0-9]+(?:\.[0-9]+)?)\b/i', $text, $matches ) ) {
     337                return $matches[1];
     338        }
     339
     340        // 3. Dimensional bead / diameter sizes: "8mm", "10mm", "12mm"
     341        if ( preg_match( '/\b([0-9]+(?:\.[0-9]+)?\s*(?:mm|cm|in|inch|inches))\b/i', $text, $matches ) ) {
     342                return trim( $matches[1] );
     343        }
     344
     345        // 4. Apparel sizes: XS, S, M, L, XL, XXL, 2XL, 3XL
     346        if ( preg_match( '/\b(XS|S|M|L|XL|XXL|2XL|3XL)\b/', $text, $matches ) ) {
     347                return $matches[1];
     348        }
     349
     350        return '';
     351}
     352
     353/**
     354 * Evaluates a Google Shopping custom label (0-4) according to configured rules.
     355 *
     356 * @param int   $slot    Custom label index (0 to 4).
     357 * @param array $product Normalized product data.
     358 * @param array $config  Plugin settings.
     359 * @return array Array with keys 'value', 'reason', 'type'.
     360 */
     361function wipf_evaluate_custom_label( $slot, $product, $config ) {
     362        $slot = absint( $slot );
     363        if ( $slot < 0 || $slot > 4 ) {
     364                return array( 'value' => '', 'reason' => 'Invalid slot', 'type' => 'disabled' );
     365        }
     366
     367        $type     = ! empty( $config["custom_label_{$slot}_type"] ) ? $config["custom_label_{$slot}_type"] : 'disabled';
     368        $rule_val = ! empty( $config["custom_label_{$slot}_value"] ) ? trim( $config["custom_label_{$slot}_value"] ) : '';
     369        $fallback = ! empty( $config["custom_label_{$slot}_fallback"] ) ? trim( $config["custom_label_{$slot}_fallback"] ) : '';
     370
     371        $result = '';
     372        $reason = '';
     373
     374        switch ( $type ) {
     375                case 'disabled':
     376                        return array(
     377                                'value'  => '',
     378                                'reason' => 'Slot disabled',
     379                                'type'   => 'disabled',
     380                        );
     381
     382                case 'static':
     383                        $result = $rule_val;
     384                        $reason = 'Static configured value: ' . $rule_val;
     385                        break;
     386
     387                case 'price_tier':
     388                        $price = isset( $product['price'] ) ? (float) $product['price'] : 0.0;
     389                        $curr  = ! empty( $product['currency'] ) ? $product['currency'] : ( $config['shipping_currency'] ?? 'USD' );
     390
     391                        // Check custom thresholds e.g. "50" or "30,80" or "25,50,100"
     392                        $thresholds = array( 25.0, 50.0, 100.0 );
     393                        if ( ! empty( $rule_val ) ) {
     394                                $parsed = array();
     395                                $raw_parts = explode( ',', $rule_val );
     396                                foreach ( $raw_parts as $rp ) {
     397                                        $fv = (float) trim( $rp );
     398                                        if ( $fv > 0 ) {
     399                                                $parsed[] = $fv;
     400                                        }
     401                                }
     402                                if ( ! empty( $parsed ) ) {
     403                                        sort( $parsed );
     404                                        $thresholds = array_values( array_unique( $parsed ) );
     405                                }
     406                        }
     407
     408                        $count = count( $thresholds );
     409                        if ( $price < $thresholds[0] ) {
     410                                $result = 'Under ' . (int) $thresholds[0];
     411                                $reason = sprintf( 'Price %0.2f %s < %0.2f', $price, $curr, $thresholds[0] );
     412                        } elseif ( $price >= $thresholds[ $count - 1 ] ) {
     413                                $result = (int) $thresholds[ $count - 1 ] . ' and Above';
     414                                $reason = sprintf( 'Price %0.2f %s >= %0.2f', $price, $curr, $thresholds[ $count - 1 ] );
     415                        } else {
     416                                for ( $ti = 0; $ti < $count - 1; $ti++ ) {
     417                                        if ( $price >= $thresholds[ $ti ] && $price < $thresholds[ $ti + 1 ] ) {
     418                                                $result = (int) $thresholds[ $ti ] . ' to ' . (int) $thresholds[ $ti + 1 ];
     419                                                $reason = sprintf( 'Price %0.2f %s between %0.2f and %0.2f', $price, $curr, $thresholds[ $ti ], $thresholds[ $ti + 1 ] );
     420                                                break;
     421                                        }
     422                                }
     423                        }
     424                        break;
     425
     426                case 'inventory':
     427                        $status = ! empty( $product['availability'] ) ? $product['availability'] : 'in_stock';
     428                        $in_label  = 'In Stock';
     429                        $out_label = 'Out of Stock';
     430                        if ( ! empty( $rule_val ) && false !== strpos( $rule_val, ':' ) ) {
     431                                $pairs = explode( ',', $rule_val );
     432                                foreach ( $pairs as $pair ) {
     433                                        $p = array_map( 'trim', explode( ':', $pair, 2 ) );
     434                                        if ( count( $p ) === 2 ) {
     435                                                if ( 'in_stock' === $p[0] ) {
     436                                                        $in_label = $p[1];
     437                                                } elseif ( 'out_of_stock' === $p[0] ) {
     438                                                        $out_label = $p[1];
     439                                                }
     440                                        }
     441                                }
     442                        }
     443
     444                        if ( 'out_of_stock' === $status ) {
     445                                $result = $out_label;
     446                                $reason = 'Inventory status is out_of_stock';
     447                        } else {
     448                                $result = $in_label;
     449                                $reason = 'Inventory status is in_stock';
     450                        }
     451                        break;
     452
     453                case 'category':
     454                        if ( ! empty( $product['categories'] ) && is_array( $product['categories'] ) ) {
     455                                $result = reset( $product['categories'] );
     456                                $reason = 'Derived from primary category: ' . $result;
     457                        } elseif ( ! empty( $product['category'] ) ) {
     458                                $result = $product['category'];
     459                                $reason = 'Derived from product category: ' . $result;
     460                        } else {
     461                                $result = ! empty( $config['default_category'] ) ? $config['default_category'] : '';
     462                                $reason = 'Fallback to default category';
     463                        }
     464                        break;
     465
     466                case 'tag_keywords':
     467                        $tags_list = ! empty( $product['tags'] ) && is_array( $product['tags'] ) ? $product['tags'] : array();
     468                        $text_to_search = strtolower( ( $product['title'] ?? '' ) . ' ' . implode( ' ', $tags_list ) );
     469
     470                        if ( ! empty( $rule_val ) ) {
     471                                $pairs = explode( ',', $rule_val );
     472                                foreach ( $pairs as $pair ) {
     473                                        $parts = array_map( 'trim', explode( ':', $pair, 2 ) );
     474                                        $keyword = strtolower( $parts[0] );
     475                                        $label   = isset( $parts[1] ) && '' !== $parts[1] ? $parts[1] : ucwords( $parts[0] );
     476
     477                                        if ( '' !== $keyword && false !== strpos( $text_to_search, $keyword ) ) {
     478                                                $result = $label;
     479                                                $reason = "Matched keyword '{$keyword}' in title or tags";
     480                                                break;
     481                                        }
     482                                }
     483                        }
     484
     485                        if ( empty( $result ) ) {
     486                                $reason = 'No matching tag or keyword found';
     487                        }
     488                        break;
     489
     490                case 'custom_meta':
     491                        $meta_key = $rule_val;
     492                        if ( ! empty( $meta_key ) && ! empty( $product['id'] ) && function_exists( 'get_post_meta' ) ) {
     493                                $meta_val = get_post_meta( $product['id'], $meta_key, true );
     494                                if ( empty( $meta_val ) && ! empty( $product['item_group_id'] ) ) {
     495                                        $meta_val = get_post_meta( $product['item_group_id'], $meta_key, true );
     496                                }
     497                                if ( ! empty( $meta_val ) && is_scalar( $meta_val ) ) {
     498                                        $result = (string) $meta_val;
     499                                        $reason = "Retrieved from post meta '{$meta_key}'";
     500                                } else {
     501                                        $reason = "Post meta '{$meta_key}' is empty";
     502                                }
     503                        } else {
     504                                $reason = 'Custom meta key not specified';
     505                        }
     506                        break;
     507
     508                default:
     509                        $reason = 'Unknown rule type: ' . $type;
     510                        break;
     511        }
     512
     513        // Apply fallback if result is empty
     514        if ( '' === $result && '' !== $fallback ) {
     515                $result = $fallback;
     516                $reason .= ' (Fallback applied: ' . $fallback . ')';
     517        }
     518
     519        // Google Shopping max length for custom_label_0-4 is 100 characters
     520        if ( '' !== $result ) {
     521                $clean_result = sanitize_text_field( $result );
     522                if ( function_exists( 'mb_strlen' ) ) {
     523                        if ( mb_strlen( $clean_result, 'UTF-8' ) > 100 ) {
     524                                $clean_result = mb_substr( $clean_result, 0, 100, 'UTF-8' );
     525                        }
     526                } else {
     527                        $clean_result = substr( $clean_result, 0, 100 );
     528                }
     529                $result = $clean_result;
     530        }
     531
     532        return array(
     533                'value'  => $result,
     534                'reason' => $reason,
     535                'type'   => $type,
     536        );
     537}
     538
     539/**
    156540 * Safe XML entity escaper.
    157541 *
    … …  
    165549/**
    166550 * Fetches normalized product catalog from active e-commerce platforms.
    167  * Supports SureCart and WooCommerce seamlessly.
     551 * Supports SureCart and WooCommerce seamlessly including product variations.
    168552 *
    169553 * @param int $limit Maximum number of products to return.
    … …  
    176560                $limit = 500;
    177561        }
     562
     563        $settings = wipf_get_settings();
    178564
    179565        // 1. Check SureCart products (sc_product)
    … …  
    239625                                }
    240626
     627                                // Categories & Tags
     628                                $categories = array();
     629                                if ( function_exists( 'wp_get_post_terms' ) ) {
     630                                        $sc_terms = wp_get_post_terms( $post_id, 'sc_collection', array( 'fields' => 'names' ) );
     631                                        if ( is_array( $sc_terms ) ) {
     632                                                $categories = $sc_terms;
     633                                        }
     634                                }
     635
     636                                $tags = array();
     637                                if ( function_exists( 'wp_get_post_terms' ) ) {
     638                                        $sc_tag_terms = wp_get_post_terms( $post_id, 'sc_tag', array( 'fields' => 'names' ) );
     639                                        if ( is_array( $sc_tag_terms ) ) {
     640                                                $tags = $sc_tag_terms;
     641                                        }
     642                                }
     643
     644                                // Variant attributes
     645                                $parent_id = get_post_meta( $post_id, 'sc_parent_id', true );
     646
     647                                $sc_color = get_post_meta( $post_id, 'sc_color', true );
     648                                if ( empty( $sc_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) {
     649                                        $sc_color = wipf_detect_color( $title, $content );
     650                                }
     651
     652                                $sc_size = get_post_meta( $post_id, 'sc_size', true );
     653                                if ( empty( $sc_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) {
     654                                        $sc_size = wipf_detect_size( $title, $content );
     655                                }
     656
     657                                $sc_mat = get_post_meta( $post_id, 'sc_material', true );
     658                                if ( empty( $sc_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) {
     659                                        $sc_mat = wipf_detect_material( $title, $content );
     660                                }
     661
     662                                $sc_pat = get_post_meta( $post_id, 'sc_pattern', true );
     663                                if ( empty( $sc_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) {
     664                                        $sc_pat = wipf_detect_pattern( $title, $content );
     665                                }
     666
    241667                                $products[] = array(
    242668                                        'id'             => $post_id,
     669                                        'item_group_id'  => ! empty( $parent_id ) ? (string) $parent_id : '',
    243670                                        'title'          => $title,
    244671                                        'description'    => $content,
    … …  
    250677                                        'availability'   => $inv_status,
    251678                                        'source'         => 'surecart',
     679                                        'color'          => $sc_color,
     680                                        'size'           => $sc_size,
     681                                        'material'       => $sc_mat,
     682                                        'pattern'        => $sc_pat,
     683                                        'categories'     => $categories,
     684                                        'tags'           => $tags,
    252685                                );
     686
     687                                if ( count( $products ) >= $limit ) {
     688                                        break;
     689                                }
    253690                        }
    254691                        wp_reset_postdata();
    … …  
    257694
    258695        // 2. Check WooCommerce products (product)
    259         if ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ) {
     696        if ( ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ) && count( $products ) < $limit ) {
    260697                $wc_args = array(
    261698                        'post_type'      => 'product',
    262699                        'post_status'    => 'publish',
    263                         'posts_per_page' => $limit,
     700                        'posts_per_page' => $limit - count( $products ),
    264701                        'orderby'        => 'ID',
    265702                        'order'          => 'DESC',
    … …  
    284721                                $inv_status = 'in_stock';
    285722
    286                                 if ( function_exists( 'wc_get_product' ) ) {
    287                                         $wc_prod = wc_get_product( $post_id );
    288                                         if ( $wc_prod ) {
    289                                                 $price = $wc_prod->get_price();
    290                                                 $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock';
     723                                // Categories & Tags
     724                                $categories = array();
     725                                if ( function_exists( 'wp_get_post_terms' ) ) {
     726                                        $wc_terms = wp_get_post_terms( $post_id, 'product_cat', array( 'fields' => 'names' ) );
     727                                        if ( is_array( $wc_terms ) ) {
     728                                                $categories = $wc_terms;
    291729                                        }
    292                                 } else {
    293                                         $raw_price = get_post_meta( $post_id, '_price', true );
    294                                         if ( is_numeric( $raw_price ) ) {
    295                                                 $price = (float) $raw_price;
     730                                }
     731
     732                                $tags = array();
     733                                if ( function_exists( 'wp_get_post_terms' ) ) {
     734                                        $wc_tag_terms = wp_get_post_terms( $post_id, 'product_tag', array( 'fields' => 'names' ) );
     735                                        if ( is_array( $wc_tag_terms ) ) {
     736                                                $tags = $wc_tag_terms;
    296737                                        }
    297738                                }
    … …  
    314755                                }
    315756
     757                                // Check WooCommerce Product object & Variable product handling
     758                                $wc_prod = function_exists( 'wc_get_product' ) ? wc_get_product( $post_id ) : null;
     759                                if ( $wc_prod ) {
     760                                        // Variable products: expand into variations with item_group_id
     761                                        if ( method_exists( $wc_prod, 'is_type' ) && $wc_prod->is_type( 'variable' ) ) {
     762                                                $variation_ids = method_exists( $wc_prod, 'get_children' ) ? $wc_prod->get_children() : array();
     763                                                if ( ! empty( $variation_ids ) ) {
     764                                                        foreach ( $variation_ids as $var_id ) {
     765                                                                $var_obj = wc_get_product( $var_id );
     766                                                                if ( ! $var_obj ) {
     767                                                                        continue;
     768                                                                }
     769
     770                                                                $var_price = $var_obj->get_price();
     771                                                                $var_inv   = $var_obj->is_in_stock() ? 'in_stock' : 'out_of_stock';
     772                                                                $var_link  = $var_obj->get_permalink() ?: $permalink;
     773
     774                                                                $var_img_id = $var_obj->get_image_id();
     775                                                                $var_image  = $var_img_id ? wp_get_attachment_url( $var_img_id ) : $image_url;
     776
     777                                                                // Variation attributes
     778                                                                $var_color = $var_obj->get_attribute( 'pa_color' ) ?: $var_obj->get_attribute( 'color' );
     779                                                                if ( empty( $var_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) {
     780                                                                        $var_color = wipf_detect_color( $var_obj->get_name() . ' ' . $title, $content );
     781                                                                }
     782
     783                                                                $var_size = $var_obj->get_attribute( 'pa_size' ) ?: $var_obj->get_attribute( 'size' );
     784                                                                if ( empty( $var_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) {
     785                                                                        $var_size = wipf_detect_size( $var_obj->get_name() . ' ' . $title, $content );
     786                                                                }
     787
     788                                                                $var_mat = $var_obj->get_attribute( 'pa_material' ) ?: $var_obj->get_attribute( 'material' );
     789                                                                if ( empty( $var_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) {
     790                                                                        $var_mat = wipf_detect_material( $var_obj->get_name() . ' ' . $title, $content );
     791                                                                }
     792
     793                                                                $var_pat = $var_obj->get_attribute( 'pa_pattern' ) ?: $var_obj->get_attribute( 'pattern' );
     794                                                                if ( empty( $var_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) {
     795                                                                        $var_pat = wipf_detect_pattern( $var_obj->get_name() . ' ' . $title, $content );
     796                                                                }
     797
     798                                                                $products[] = array(
     799                                                                        'id'             => $var_id,
     800                                                                        'item_group_id'  => (string) $post_id,
     801                                                                        'title'          => $var_obj->get_name() ?: $title,
     802                                                                        'description'    => $content,
     803                                                                        'permalink'      => $var_link,
     804                                                                        'image_url'      => $var_image,
     805                                                                        'gallery_images' => $gallery_images,
     806                                                                        'price'          => number_format( (float) $var_price, 2, '.', '' ),
     807                                                                        'currency'       => $currency,
     808                                                                        'availability'   => $var_inv,
     809                                                                        'source'         => 'woocommerce',
     810                                                                        'color'          => $var_color,
     811                                                                        'size'           => $var_size,
     812                                                                        'material'       => $var_mat,
     813                                                                        'pattern'        => $var_pat,
     814                                                                        'categories'     => $categories,
     815                                                                        'tags'           => $tags,
     816                                                                );
     817
     818                                                                if ( count( $products ) >= $limit ) {
     819                                                                        break 2;
     820                                                                }
     821                                                        }
     822                                                        continue; // Variations added, skip parent product
     823                                                }
     824                                        }
     825
     826                                        // Simple product
     827                                        $price = $wc_prod->get_price();
     828                                        $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock';
     829                                } else {
     830                                        $raw_price = get_post_meta( $post_id, '_price', true );
     831                                        if ( is_numeric( $raw_price ) ) {
     832                                                $price = (float) $raw_price;
     833                                        }
     834                                }
     835
     836                                // Simple product attributes
     837                                $prod_color = '';
     838                                $prod_size  = '';
     839                                $prod_mat   = '';
     840                                $prod_pat   = '';
     841
     842                                if ( $wc_prod && method_exists( $wc_prod, 'get_attribute' ) ) {
     843                                        $prod_color = $wc_prod->get_attribute( 'pa_color' ) ?: $wc_prod->get_attribute( 'color' );
     844                                        $prod_size  = $wc_prod->get_attribute( 'pa_size' ) ?: $wc_prod->get_attribute( 'size' );
     845                                        $prod_mat   = $wc_prod->get_attribute( 'pa_material' ) ?: $wc_prod->get_attribute( 'material' );
     846                                        $prod_pat   = $wc_prod->get_attribute( 'pa_pattern' ) ?: $wc_prod->get_attribute( 'pattern' );
     847                                }
     848
     849                                if ( empty( $prod_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) {
     850                                        $prod_color = wipf_detect_color( $title, $content );
     851                                }
     852                                if ( empty( $prod_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) {
     853                                        $prod_size = wipf_detect_size( $title, $content );
     854                                }
     855                                if ( empty( $prod_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) {
     856                                        $prod_mat = wipf_detect_material( $title, $content );
     857                                }
     858                                if ( empty( $prod_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) {
     859                                        $prod_pat = wipf_detect_pattern( $title, $content );
     860                                }
     861
    316862                                $products[] = array(
    317863                                        'id'             => $post_id,
     864                                        'item_group_id'  => '',
    318865                                        'title'          => $title,
    319866                                        'description'    => $content,
    … …  
    325872                                        'availability'   => $inv_status,
    326873                                        'source'         => 'woocommerce',
     874                                        'color'          => $prod_color,
     875                                        'size'           => $prod_size,
     876                                        'material'       => $prod_mat,
     877                                        'pattern'        => $prod_pat,
     878                                        'categories'     => $categories,
     879                                        'tags'           => $tags,
    327880                                );
     881
     882                                if ( count( $products ) >= $limit ) {
     883                                        break;
     884                                }
    328885                        }
    329886                        wp_reset_postdata();
    … …  
    353910        $ship_cost       = ! empty( $config['shipping_cost'] ) ? $config['shipping_cost'] : '0.00';
    354911        $ship_curr       = ! empty( $config['shipping_currency'] ) ? $config['shipping_currency'] : 'USD';
     912        $enable_labels   = ( 'no' !== ( $config['enable_custom_labels'] ?? 'yes' ) );
    355913
    356914        $xml  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    … …  
    367925                $price_val   = $item['price'];
    368926                $curr_val    = ! empty( $item['currency'] ) ? $item['currency'] : $ship_curr;
    369                 $item_color  = $auto_color ? wipf_detect_color( $title, $desc ) : '';
    370 
    371                 // Pet classification heuristic
     927
     928                // Color priority: item attribute > heuristic
     929                $item_color  = ! empty( $item['color'] ) ? $item['color'] : ( $auto_color ? wipf_detect_color( $title, $desc ) : '' );
     930
     931                // Category classification heuristic
    372932                $category = $def_category;
     933                if ( ! empty( $item['categories'] ) && is_array( $item['categories'] ) ) {
     934                        $category = implode( ' > ', $item['categories'] );
     935                }
    373936                if ( false !== stripos( $title, 'collar' ) || false !== stripos( $title, 'pet' ) || false !== stripos( $title, 'dog' ) || false !== stripos( $title, 'cat' ) ) {
    374937                        $category = 'Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses';
    … …  
    377940                $xml .= "    <item>\n";
    378941                $xml .= '      <g:id>' . wipf_xml_escape( $item['id'] ) . "</g:id>\n";
     942
     943                // Variant item_group_id grouping
     944                if ( ! empty( $item['item_group_id'] ) ) {
     945                        $xml .= '      <g:item_group_id>' . wipf_xml_escape( $item['item_group_id'] ) . "</g:item_group_id>\n";
     946                }
     947
    379948                $xml .= '      <g:title>' . wipf_xml_escape( $title ) . "</g:title>\n";
    380949                $xml .= '      <g:description>' . wipf_xml_escape( $desc ) . "</g:description>\n";
    … …  
    400969                        $xml .= '      <g:color>' . wipf_xml_escape( $item_color ) . "</g:color>\n";
    401970                }
     971                if ( ! empty( $item['size'] ) ) {
     972                        $xml .= '      <g:size>' . wipf_xml_escape( $item['size'] ) . "</g:size>\n";
     973                }
     974                if ( ! empty( $item['material'] ) ) {
     975                        $xml .= '      <g:material>' . wipf_xml_escape( $item['material'] ) . "</g:material>\n";
     976                }
     977                if ( ! empty( $item['pattern'] ) ) {
     978                        $xml .= '      <g:pattern>' . wipf_xml_escape( $item['pattern'] ) . "</g:pattern>\n";
     979                }
     980
    402981                if ( ! empty( $def_age_group ) ) {
    403982                        $xml .= '      <g:age_group>' . wipf_xml_escape( $def_age_group ) . "</g:age_group>\n";
    … …  
    405984                if ( ! empty( $def_gender ) ) {
    406985                        $xml .= '      <g:gender>' . wipf_xml_escape( $def_gender ) . "</g:gender>\n";
     986                }
     987
     988                // Inject Custom Labels (custom_label_0 to custom_label_4)
     989                if ( $enable_labels ) {
     990                        for ( $slot = 0; $slot <= 4; $slot++ ) {
     991                                $label_data = wipf_evaluate_custom_label( $slot, $item, $config );
     992                                if ( ! empty( $label_data['value'] ) ) {
     993                                        $xml .= '      <g:custom_label_' . $slot . '>' . wipf_xml_escape( $label_data['value'] ) . '</g:custom_label_' . $slot . ">\n";
     994                                }
     995                        }
    407996                }
    408997
    … …  
    4631052
    4641053/**
     1054 * Detailed diagnostic inspector for a specific product ID or product data array.
     1055 *
     1056 * @param int|array  $product Product ID or product array.
     1057 * @param array|null $config  Optional configuration override.
     1058 * @return array Diagnostic report.
     1059 */
     1060function wipf_diagnose_product( $product, $config = null ) {
     1061        if ( null === $config ) {
     1062                $config = wipf_get_settings();
     1063        }
     1064
     1065        $item = is_array( $product ) ? $product : null;
     1066        if ( null === $item && is_numeric( $product ) ) {
     1067                $limit = 200;
     1068                $all_prods = wipf_collect_products( $limit );
     1069                foreach ( $all_prods as $p ) {
     1070                        if ( (int) $p['id'] === (int) $product ) {
     1071                                $item = $p;
     1072                                break;
     1073                        }
     1074                }
     1075                if ( null === $item && function_exists( 'get_post' ) ) {
     1076                        $post = get_post( (int) $product );
     1077                        if ( $post && 'publish' === ( $post->post_status ?? '' ) ) {
     1078                                $title = get_the_title( $post );
     1079                                $content = wp_strip_all_tags( get_the_excerpt( $post ) ?: ( get_the_content( null, false, $post ) ?: $title ) );
     1080                                $raw_price = get_post_meta( $post->ID, '_price', true );
     1081                                $price = is_numeric( $raw_price ) ? (float) $raw_price : 0.0;
     1082                                $image = get_the_post_thumbnail_url( $post->ID, 'full' ) ?: '';
     1083
     1084                                $categories = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_cat', array( 'fields' => 'names' ) ) : array();
     1085                                $tags = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_tag', array( 'fields' => 'names' ) ) : array();
     1086
     1087                                $item = array(
     1088                                        'id'             => $post->ID,
     1089                                        'item_group_id'  => '',
     1090                                        'title'          => $title,
     1091                                        'description'    => $content,
     1092                                        'permalink'      => get_permalink( $post->ID ),
     1093                                        'image_url'      => $image,
     1094                                        'gallery_images' => array(),
     1095                                        'price'          => number_format( $price, 2, '.', '' ),
     1096                                        'currency'       => $config['shipping_currency'] ?? 'USD',
     1097                                        'availability'   => 'in_stock',
     1098                                        'source'         => $post->post_type,
     1099                                        'color'          => wipf_detect_color( $title, $content ),
     1100                                        'size'           => wipf_detect_size( $title, $content ),
     1101                                        'material'       => wipf_detect_material( $title, $content ),
     1102                                        'pattern'        => wipf_detect_pattern( $title, $content ),
     1103                                        'categories'     => is_array( $categories ) ? $categories : array(),
     1104                                        'tags'           => is_array( $tags ) ? $tags : array(),
     1105                                );
     1106                        }
     1107                }
     1108        }
     1109
     1110        if ( empty( $item ) ) {
     1111                return array(
     1112                        'found' => false,
     1113                        'error' => __( 'Product not found or not published in catalog.', 'world-inspire-product-feed-for-google-shopping' ),
     1114                );
     1115        }
     1116
     1117        $labels = array();
     1118        for ( $i = 0; $i <= 4; $i++ ) {
     1119                $labels[ "custom_label_{$i}" ] = wipf_evaluate_custom_label( $i, $item, $config );
     1120        }
     1121
     1122        // Generate XML snippet for just this item
     1123        $xml_doc = wipf_build_google_shopping_xml( array( $item ), $config );
     1124        $xml_snippet = '';
     1125        if ( preg_match( '/<item>.*?<\/item>/s', $xml_doc, $matches ) ) {
     1126                $xml_snippet = trim( $matches[0] );
     1127        }
     1128
     1129        return array(
     1130                'found'       => true,
     1131                'product'     => $item,
     1132                'labels'      => $labels,
     1133                'xml_snippet' => $xml_snippet,
     1134        );
     1135}
     1136
     1137/**
    4651138 * Streams XML document with anti-caching reverse-proxy protection.
    4661139 *
    … …  
    4821155
    4831156/**
    484  * Registers REST API endpoints for Google Shopping and Trustmate.
     1157 * Registers REST API endpoints for Google Shopping, Trustmate, and Status.
    4851158 */
    4861159function wipf_register_rest_routes() {
    … …  
    5251198                                return new WP_REST_Response(
    5261199                                        array(
    527                                                 'enabled'            => true,
    528                                                 'status'             => 'healthy',
    529                                                 'version'            => WIPF_VERSION,
    530                                                 'surecart_detected'  => post_type_exists( 'sc_product' ),
     1200                                                'enabled'              => true,
     1201                                                'status'               => 'healthy',
     1202                                                'version'              => WIPF_VERSION,
     1203                                                'surecart_detected'    => post_type_exists( 'sc_product' ),
    5311204                                                'woocommerce_detected' => ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ),
    532                                                 'sample_count'       => count( $products ),
     1205                                                'sample_count'         => count( $products ),
     1206                                                'custom_labels_active' => ( 'no' !== ( wipf_get_settings()['enable_custom_labels'] ?? 'yes' ) ),
    5331207                                        ),
    5341208                                        200
    … …  
    5731247        $output['identifier_exists']     = ( 'yes' === ( $input['identifier_exists'] ?? 'no' ) ) ? 'yes' : 'no';
    5741248        $output['auto_detect_color']     = ( 'yes' === ( $input['auto_detect_color'] ?? 'yes' ) ) ? 'yes' : 'no';
     1249        $output['auto_detect_material']  = ( 'yes' === ( $input['auto_detect_material'] ?? 'yes' ) ) ? 'yes' : 'no';
     1250        $output['auto_detect_size']      = ( 'yes' === ( $input['auto_detect_size'] ?? 'yes' ) ) ? 'yes' : 'no';
     1251        $output['auto_detect_pattern']   = ( 'yes' === ( $input['auto_detect_pattern'] ?? 'yes' ) ) ? 'yes' : 'no';
    5751252        $output['shipping_countries']    = ! empty( $input['shipping_countries'] ) ? sanitize_text_field( $input['shipping_countries'] ) : $defaults['shipping_countries'];
    5761253        $output['shipping_service_name'] = ! empty( $input['shipping_service_name'] ) ? sanitize_text_field( $input['shipping_service_name'] ) : $defaults['shipping_service_name'];
    … …  
    5801257        $output['enable_trustmate_feed'] = ( 'yes' === ( $input['enable_trustmate_feed'] ?? 'yes' ) ) ? 'yes' : 'no';
    5811258        $output['items_per_feed']        = absint( $input['items_per_feed'] ?? 500 );
     1259
     1260        // Custom Labels (custom_label_0 to custom_label_4)
     1261        $output['enable_custom_labels']  = ( 'yes' === ( $input['enable_custom_labels'] ?? 'yes' ) ) ? 'yes' : 'no';
     1262        $valid_rule_types = array( 'disabled', 'static', 'price_tier', 'inventory', 'category', 'tag_keywords', 'custom_meta' );
     1263
     1264        for ( $i = 0; $i <= 4; $i++ ) {
     1265                $type_key     = "custom_label_{$i}_type";
     1266                $val_key      = "custom_label_{$i}_value";
     1267                $fallback_key = "custom_label_{$i}_fallback";
     1268
     1269                $type = isset( $input[ $type_key ] ) && in_array( $input[ $type_key ], $valid_rule_types, true ) ? sanitize_key( $input[ $type_key ] ) : 'disabled';
     1270                $val  = isset( $input[ $val_key ] ) ? sanitize_text_field( $input[ $val_key ] ) : '';
     1271                $fb   = isset( $input[ $fallback_key ] ) ? sanitize_text_field( $input[ $fallback_key ] ) : '';
     1272
     1273                $output[ $type_key ]     = $type;
     1274                $output[ $val_key ]      = $val;
     1275                $output[ $fallback_key ] = $fb;
     1276        }
    5821277
    5831278        return $output;
    … …  
    6141309        ?>
    6151310        <div class="wrap">
    616                 <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?></h1>
     1311                <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?> <span style="font-size: 14px; background: #2271b1; color: #fff; padding: 2px 8px; border-radius: 4px; vertical-align: middle;">v<?php echo esc_html( WIPF_VERSION ); ?></span></h1>
    6171312                <p class="description">
    6181313                        <?php echo esc_html__( 'Zero-configuration, high-performance real-time XML product feeds engineered for Google Merchant Center and Trustmate review synchronization.', 'world-inspire-product-feed-for-google-shopping' ); ?>
    … …  
    6241319                        </a>
    6251320                        <a href="?page=wipf-settings&tab=google" class="nav-tab <?php echo 'google' === $active_tab ? 'nav-tab-active' : ''; ?>">
    626                                 <?php echo esc_html__( 'Google Shopping Rules', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1321                                <?php echo esc_html__( 'Google Shopping Rules & Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?>
    6271322                        </a>
    6281323                        <a href="?page=wipf-settings&tab=shipping" class="nav-tab <?php echo 'shipping' === $active_tab ? 'nav-tab-active' : ''; ?>">
    … …  
    6301325                        </a>
    6311326                        <a href="?page=wipf-settings&tab=diagnostic" class="nav-tab <?php echo 'diagnostic' === $active_tab ? 'nav-tab-active' : ''; ?>">
    632                                 <?php echo esc_html__( 'Live Diagnostics & Preview', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1327                                <?php echo esc_html__( 'Live Diagnostics & Inspector', 'world-inspire-product-feed-for-google-shopping' ); ?>
    6331328                        </a>
    6341329                </h2>
    6351330
    6361331                <?php if ( 'endpoints' === $active_tab ) : ?>
    637                         <div class="card" style="max-width: 900px; margin-top: 20px; padding: 20px;">
     1332                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
    6381333                                <h3><?php echo esc_html__( 'Authoritative Feed URLs', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    6391334                                <p><?php echo esc_html__( 'Copy these extension-free dynamic REST endpoints into Google Merchant Center and Trustmate. They automatically bypass reverse-proxy static file caches and update in real-time.', 'world-inspire-product-feed-for-google-shopping' ); ?></p>
    … …  
    6641359                        </div>
    6651360                <?php elseif ( 'google' === $active_tab || 'shipping' === $active_tab ) : ?>
    666                         <form method="post" action="options.php" style="max-width: 900px; margin-top: 20px;">
     1361                        <form method="post" action="options.php" style="max-width: 960px; margin-top: 20px;">
    6671362                                <?php
    6681363                                settings_fields( 'wipf_settings_group' );
    6691364                                ?>
    670                                 <div class="card" style="padding: 20px;">
    671                                         <?php if ( 'google' === $active_tab ) : ?>
     1365                                <?php if ( 'google' === $active_tab ) : ?>
     1366                                        <div class="card" style="padding: 20px; margin-bottom: 20px;">
    6721367                                                <h3><?php echo esc_html__( 'Google Shopping Catalog Standards', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    6731368                                                <table class="form-table" role="presentation">
    … …  
    6921387                                                                                <option value="kids" <?php selected( $settings['default_age_group'], 'kids' ); ?>><?php echo esc_html__( 'Kids', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
    6931388                                                                                <option value="toddler" <?php selected( $settings['default_age_group'], 'toddler' ); ?>><?php echo esc_html__( 'Toddler', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1389                                                                                <option value="infant" <?php selected( $settings['default_age_group'], 'infant' ); ?>><?php echo esc_html__( 'Infant', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1390                                                                                <option value="newborn" <?php selected( $settings['default_age_group'], 'newborn' ); ?>><?php echo esc_html__( 'Newborn', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
    6941391                                                                        </select>
    6951392                                                                </td>
    … …  
    7051402                                                                </td>
    7061403                                                        </tr>
     1404                                                </table>
     1405                                        </div>
     1406
     1407                                        <div class="card" style="padding: 20px; margin-bottom: 20px;">
     1408                                                <h3><?php echo esc_html__( 'Variant & Attribute Optimization', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
     1409                                                <p class="description">
     1410                                                        <?php echo esc_html__( 'Extract and optimize essential Google Shopping attributes from WooCommerce and SureCart. WooCommerce variable products are automatically exported as individual items linked by <g:item_group_id>.', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1411                                                </p>
     1412                                                <table class="form-table" role="presentation">
    7071413                                                        <tr>
    708                                                                 <th scope="row"><label for="wipf_auto_color"><?php echo esc_html__( 'Smart Color Extraction', 'world-inspire-product-feed-for-google-shopping' ); ?></label></th>
     1414                                                                <th scope="row"><?php echo esc_html__( 'Smart Attribute Detection', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    7091415                                                                <td>
    710                                                                         <label>
    711                                                                                 <input type="checkbox" id="wipf_auto_color" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> />
    712                                                                                 <?php echo esc_html__( 'Automatically infer and populate Google standard colors from title and description', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1416                                                                        <label style="display: block; margin-bottom: 8px;">
     1417                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> />
     1418                                                                                <strong><?php echo esc_html__( 'Color Extraction (<g:color>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Infer Google standard color names from attributes, titles, or descriptions (over 35 jewelry/apparel shades).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1419                                                                        </label>
     1420                                                                        <label style="display: block; margin-bottom: 8px;">
     1421                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_material]" value="yes" <?php checked( $settings['auto_detect_material'] ?? 'yes', 'yes' ); ?> />
     1422                                                                                <strong><?php echo esc_html__( 'Material Extraction (<g:material>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect materials (Gold, Sterling Silver, Leather, Cotton, Gemstones, Titanium, Stainless Steel).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1423                                                                        </label>
     1424                                                                        <label style="display: block; margin-bottom: 8px;">
     1425                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_size]" value="yes" <?php checked( $settings['auto_detect_size'] ?? 'yes', 'yes' ); ?> />
     1426                                                                                <strong><?php echo esc_html__( 'Size Extraction (<g:size>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Extract sizes from variation attributes or titles (e.g. Size 7, One Size, 8mm, S, M, L, XL).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1427                                                                        </label>
     1428                                                                        <label style="display: block; margin-bottom: 8px;">
     1429                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_pattern]" value="yes" <?php checked( $settings['auto_detect_pattern'] ?? 'yes', 'yes' ); ?> />
     1430                                                                                <strong><?php echo esc_html__( 'Pattern Extraction (<g:pattern>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect patterns (Solid, Striped, Floral, Beaded, Polka Dot, Geometric).', 'world-inspire-product-feed-for-google-shopping' ); ?>
    7131431                                                                        </label>
    7141432                                                                </td>
    7151433                                                        </tr>
    7161434                                                </table>
    717                                         <?php elseif ( 'shipping' === $active_tab ) : ?>
     1435                                        </div>
     1436
     1437                                        <div class="card" style="padding: 20px; margin-bottom: 20px;">
     1438                                                <h3><?php echo esc_html__( 'Google Shopping Custom Labels (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
     1439                                                <p class="description">
     1440                                                        <?php echo esc_html__( 'Custom labels allow you to segment and filter products in Google Ads and Performance Max (PMax) campaigns (e.g. by price brackets, inventory status, product category, clearance tags, or margin tiers).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1441                                                </p>
     1442                                                <p>
     1443                                                        <label>
     1444                                                                <input type="checkbox" name="wipf_feed_settings[enable_custom_labels]" value="yes" <?php checked( $settings['enable_custom_labels'] ?? 'yes', 'yes' ); ?> />
     1445                                                                <strong><?php echo esc_html__( 'Enable Custom Labels in Feed', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
     1446                                                        </label>
     1447                                                </p>
     1448
     1449                                                <table class="widefat striped" style="margin-top: 15px;">
     1450                                                        <thead>
     1451                                                                <tr>
     1452                                                                        <th style="width: 16%;"><?php echo esc_html__( 'Custom Label Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1453                                                                        <th style="width: 22%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1454                                                                        <th style="width: 38%;"><?php echo esc_html__( 'Rule Value / Configuration', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1455                                                                        <th style="width: 24%;"><?php echo esc_html__( 'Fallback Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1456                                                                </tr>
     1457                                                        </thead>
     1458                                                        <tbody>
     1459                                                                <?php
     1460                                                                $label_recommendations = array(
     1461                                                                        0 => array( 'name' => 'Custom Label 0', 'rec' => 'Price Tiers (< 25, 25-50, 50-100, 100+)', 'ph_val' => '25,50,100 (Thresholds)', 'ph_fb' => 'Standard' ),
     1462                                                                        1 => array( 'name' => 'Custom Label 1', 'rec' => 'Inventory Status (In Stock / Out of Stock)', 'ph_val' => 'Optional override: in_stock:Available, out_of_stock:Unavailable', 'ph_fb' => 'Out of Stock' ),
     1463                                                                        2 => array( 'name' => 'Custom Label 2', 'rec' => 'Category / Collection Name', 'ph_val' => 'Leave empty to use primary category', 'ph_fb' => 'General' ),
     1464                                                                        3 => array( 'name' => 'Custom Label 3', 'rec' => 'Promotional / Tag Keywords Match', 'ph_val' => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller', 'ph_fb' => 'Regular' ),
     1465                                                                        4 => array( 'name' => 'Custom Label 4', 'rec' => 'Static Value / Custom Post Meta Key', 'ph_val' => 'Enter static string or post_meta key', 'ph_fb' => '' ),
     1466                                                                );
     1467
     1468                                                                for ( $slot = 0; $slot <= 4; $slot++ ) :
     1469                                                                        $slot_type = $settings[ "custom_label_{$slot}_type" ] ?? 'disabled';
     1470                                                                        $slot_val  = $settings[ "custom_label_{$slot}_value" ] ?? '';
     1471                                                                        $slot_fb   = $settings[ "custom_label_{$slot}_fallback" ] ?? '';
     1472                                                                        $meta_info = $label_recommendations[ $slot ];
     1473                                                                        ?>
     1474                                                                        <tr>
     1475                                                                                <td>
     1476                                                                                        <strong><?php echo esc_html( $meta_info['name'] ); ?></strong>
     1477                                                                                        <br><span class="description" style="font-size: 11px;"><?php echo esc_html( $meta_info['rec'] ); ?></span>
     1478                                                                                </td>
     1479                                                                                <td>
     1480                                                                                        <select name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_type]" style="width: 100%;">
     1481                                                                                                <option value="disabled" <?php selected( $slot_type, 'disabled' ); ?>><?php echo esc_html__( 'Disabled', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1482                                                                                                <option value="price_tier" <?php selected( $slot_type, 'price_tier' ); ?>><?php echo esc_html__( 'Price Range Tier', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1483                                                                                                <option value="inventory" <?php selected( $slot_type, 'inventory' ); ?>><?php echo esc_html__( 'Inventory Status', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1484                                                                                                <option value="category" <?php selected( $slot_type, 'category' ); ?>><?php echo esc_html__( 'Product Category', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1485                                                                                                <option value="tag_keywords" <?php selected( $slot_type, 'tag_keywords' ); ?>><?php echo esc_html__( 'Tag & Keyword Matcher', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1486                                                                                                <option value="static" <?php selected( $slot_type, 'static' ); ?>><?php echo esc_html__( 'Static Custom String', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1487                                                                                                <option value="custom_meta" <?php selected( $slot_type, 'custom_meta' ); ?>><?php echo esc_html__( 'Custom Post Meta Field', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1488                                                                                        </select>
     1489                                                                                </td>
     1490                                                                                <td>
     1491                                                                                        <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_value]" class="large-text" value="<?php echo esc_attr( $slot_val ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_val'] ); ?>" />
     1492                                                                                </td>
     1493                                                                                <td>
     1494                                                                                        <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_fallback]" class="regular-text" value="<?php echo esc_attr( $slot_fb ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_fb'] ); ?>" />
     1495                                                                                </td>
     1496                                                                        </tr>
     1497                                                                <?php endfor; ?>
     1498                                                        </tbody>
     1499                                                </table>
     1500                                        </div>
     1501                                <?php elseif ( 'shipping' === $active_tab ) : ?>
     1502                                        <div class="card" style="padding: 20px;">
    7181503                                                <h3><?php echo esc_html__( 'Multi-Country Shipping Generator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    7191504                                                <p><?php echo esc_html__( 'Configures embedded shipping rules for international Google Shopping campaigns.', 'world-inspire-product-feed-for-google-shopping' ); ?></p>
    … …  
    7411526                                                        </tr>
    7421527                                                </table>
    743                                         <?php endif; ?>
    744                                 </div>
     1528                                        </div>
     1529                                <?php endif; ?>
    7451530                                <?php submit_button(); ?>
    7461531                        </form>
    7471532                <?php elseif ( 'diagnostic' === $active_tab ) : ?>
    748                         <div class="card" style="max-width: 900px; margin-top: 20px; padding: 20px;">
    749                                 <h3><?php echo esc_html__( 'Live Feed Diagnostic Audit', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    750                                 <?php
    751                                 $sample_products = wipf_collect_products( 5 );
    752                                 $surecart_ok     = post_type_exists( 'sc_product' );
    753                                 $woocommerce_ok  = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) );
    754                                 ?>
     1533                        <?php
     1534                        $surecart_ok     = post_type_exists( 'sc_product' );
     1535                        $woocommerce_ok  = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) );
     1536                        $sample_products = wipf_collect_products( 5 );
     1537
     1538                        $inspect_id = isset( $_GET['inspect_id'] ) ? absint( $_GET['inspect_id'] ) : ( ! empty( $sample_products[0]['id'] ) ? (int) $sample_products[0]['id'] : 0 );
     1539                        $audit_data = $inspect_id > 0 ? wipf_diagnose_product( $inspect_id, $settings ) : null;
     1540                        ?>
     1541                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
     1542                                <h3><?php echo esc_html__( 'Live Feed Engine Status', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    7551543                                <p>
    7561544                                        <strong><?php echo esc_html__( 'SureCart Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
    757                                         <?php echo $surecart_ok ? '<span style="color:green;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
     1545                                        <?php echo $surecart_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
    7581546                                        &nbsp;|&nbsp;
    7591547                                        <strong><?php echo esc_html__( 'WooCommerce Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
    760                                         <?php echo $woocommerce_ok ? '<span style="color:green;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
     1548                                        <?php echo $woocommerce_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
     1549                                        &nbsp;|&nbsp;
     1550                                        <strong><?php echo esc_html__( 'Catalog Sample Available:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
     1551                                        <code><?php echo esc_html( count( $sample_products ) ); ?></code>
    7611552                                </p>
    762                                 <p>
    763                                         <strong><?php echo esc_html__( 'Products Found in Catalog:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
    764                                         <?php echo esc_html( count( $sample_products ) ); ?> (<?php echo esc_html__( 'showing first 5', 'world-inspire-product-feed-for-google-shopping' ); ?>)
     1553                        </div>
     1554
     1555                        <!-- Product Inspector & XML Simulator -->
     1556                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
     1557                                <h3><?php echo esc_html__( 'Product Inspector & Diagnostic Simulator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
     1558                                <p class="description">
     1559                                        <?php echo esc_html__( 'Enter any Product ID to audit its exact extracted attributes, custom labels evaluation logic, and preview the generated Google Shopping <item> XML element.', 'world-inspire-product-feed-for-google-shopping' ); ?>
    7651560                                </p>
    7661561
     1562                                <form method="get" action="" style="margin: 15px 0 20px 0;">
     1563                                        <input type="hidden" name="page" value="wipf-settings" />
     1564                                        <input type="hidden" name="tab" value="diagnostic" />
     1565                                        <label for="inspect_id"><strong><?php echo esc_html__( 'Test Product ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong></label>
     1566                                        <input type="number" id="inspect_id" name="inspect_id" value="<?php echo esc_attr( $inspect_id ? (string) $inspect_id : '' ); ?>" placeholder="e.g. 790" style="width: 140px; margin: 0 10px;" />
     1567                                        <button type="submit" class="button button-primary"><?php echo esc_html__( 'Inspect Product', 'world-inspire-product-feed-for-google-shopping' ); ?></button>
     1568                                </form>
     1569
     1570                                <?php if ( $audit_data && ! empty( $audit_data['found'] ) ) : ?>
     1571                                        <?php
     1572                                        $p = $audit_data['product'];
     1573                                        ?>
     1574                                        <div style="background: #f0f6fc; border-left: 4px solid #72aee6; padding: 12px 16px; margin-bottom: 20px;">
     1575                                                <h4 style="margin: 0 0 8px 0;"><?php echo esc_html( $p['title'] ); ?> <span style="font-weight: normal; font-size: 13px;">(ID: <code><?php echo esc_html( $p['id'] ); ?></code> &middot; <?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?> &middot; <?php echo esc_html( $p['availability'] ); ?> &middot; Source: <code><?php echo esc_html( $p['source'] ); ?></code>)</span></h4>
     1576                                                <p style="margin: 0; font-size: 13px;">
     1577                                                        <strong><?php echo esc_html__( 'Variant Group ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <?php echo ! empty( $p['item_group_id'] ) ? '<code>' . esc_html( $p['item_group_id'] ) . '</code>' : '<em>' . esc_html__( 'None (Simple Product)', 'world-inspire-product-feed-for-google-shopping' ) . '</em>'; ?>
     1578                                                        &nbsp;|&nbsp;
     1579                                                        <strong><?php echo esc_html__( 'Color:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['color'] ?: '—' ); ?></code>
     1580                                                        &nbsp;|&nbsp;
     1581                                                        <strong><?php echo esc_html__( 'Size:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['size'] ?: '—' ); ?></code>
     1582                                                        &nbsp;|&nbsp;
     1583                                                        <strong><?php echo esc_html__( 'Material:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['material'] ?: '—' ); ?></code>
     1584                                                        &nbsp;|&nbsp;
     1585                                                        <strong><?php echo esc_html__( 'Pattern:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['pattern'] ?: '—' ); ?></code>
     1586                                                </p>
     1587                                        </div>
     1588
     1589                                        <h4><?php echo esc_html__( 'Custom Labels Evaluation Audit (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h4>
     1590                                        <table class="widefat striped" style="margin-bottom: 20px;">
     1591                                                <thead>
     1592                                                        <tr>
     1593                                                                <th style="width: 16%;"><?php echo esc_html__( 'Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1594                                                                <th style="width: 18%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1595                                                                <th style="width: 26%;"><?php echo esc_html__( 'Evaluated Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1596                                                                <th style="width: 40%;"><?php echo esc_html__( 'Matching Reason / Trace', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1597                                                        </tr>
     1598                                                </thead>
     1599                                                <tbody>
     1600                                                        <?php foreach ( $audit_data['labels'] as $slot_key => $eval ) : ?>
     1601                                                                <tr>
     1602                                                                        <td><code><?php echo esc_html( $slot_key ); ?></code></td>
     1603                                                                        <td><?php echo esc_html( ucfirst( str_replace( '_', ' ', $eval['type'] ) ) ); ?></td>
     1604                                                                        <td>
     1605                                                                                <?php if ( ! empty( $eval['value'] ) ) : ?>
     1606                                                                                        <span style="font-weight: 600; color: #135e96; background: #e7f3fa; padding: 2px 8px; border-radius: 3px;">
     1607                                                                                                <?php echo esc_html( $eval['value'] ); ?>
     1608                                                                                        </span>
     1609                                                                                <?php else : ?>
     1610                                                                                        <span style="color: #888;"><em><?php echo esc_html__( 'Empty (Omitted from XML)', 'world-inspire-product-feed-for-google-shopping' ); ?></em></span>
     1611                                                                                <?php endif; ?>
     1612                                                                        </td>
     1613                                                                        <td><small><?php echo esc_html( $eval['reason'] ); ?></small></td>
     1614                                                                </tr>
     1615                                                        <?php endforeach; ?>
     1616                                                </tbody>
     1617                                        </table>
     1618
     1619                                        <h4><?php echo esc_html__( 'Generated XML <item> Snippet Preview', 'world-inspire-product-feed-for-google-shopping' ); ?></h4>
     1620                                        <pre style="background: #2b2b2b; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; line-height: 1.5;"><code><?php echo esc_html( $audit_data['xml_snippet'] ); ?></code></pre>
     1621
     1622                                <?php elseif ( $audit_data && empty( $audit_data['found'] ) ) : ?>
     1623                                        <div class="notice notice-warning inline"><p><?php echo esc_html( $audit_data['error'] ); ?></p></div>
     1624                                <?php endif; ?>
     1625                        </div>
     1626
     1627                        <!-- Catalog Overview Sample Table -->
     1628                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
     1629                                <h3><?php echo esc_html__( 'Catalog Sample Inspection (First 5 Items)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    7671630                                <?php if ( ! empty( $sample_products ) ) : ?>
    7681631                                        <table class="widefat striped" style="margin-top: 15px;">
    … …  
    7721635                                                                <th><?php echo esc_html__( 'Title', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    7731636                                                                <th><?php echo esc_html__( 'Price', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    774                                                                 <th><?php echo esc_html__( 'Availability', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    775                                                                 <th><?php echo esc_html__( 'Detected Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    776                                                                 <th><?php echo esc_html__( 'Engine', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1637                                                                <th><?php echo esc_html__( 'Group ID', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1638                                                                <th><?php echo esc_html__( 'Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1639                                                                <th><?php echo esc_html__( 'Size', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1640                                                                <th><?php echo esc_html__( 'Material', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1641                                                                <th><?php echo esc_html__( 'Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1642                                                                <th><?php echo esc_html__( 'Action', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    7771643                                                        </tr>
    7781644                                                </thead>
    7791645                                                <tbody>
    7801646                                                        <?php foreach ( $sample_products as $p ) : ?>
     1647                                                                <?php
     1648                                                                $p_eval0 = wipf_evaluate_custom_label( 0, $p, $settings );
     1649                                                                $p_eval1 = wipf_evaluate_custom_label( 1, $p, $settings );
     1650                                                                $label_summary = array();
     1651                                                                if ( ! empty( $p_eval0['value'] ) ) {
     1652                                                                        $label_summary[] = 'L0: ' . $p_eval0['value'];
     1653                                                                }
     1654                                                                if ( ! empty( $p_eval1['value'] ) ) {
     1655                                                                        $label_summary[] = 'L1: ' . $p_eval1['value'];
     1656                                                                }
     1657                                                                ?>
    7811658                                                                <tr>
    7821659                                                                        <td><code><?php echo esc_html( $p['id'] ); ?></code></td>
    7831660                                                                        <td><strong><?php echo esc_html( $p['title'] ); ?></strong></td>
    7841661                                                                        <td><?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?></td>
    785                                                                         <td><?php echo esc_html( $p['availability'] ); ?></td>
    786                                                                         <td><?php echo esc_html( wipf_detect_color( $p['title'], $p['description'] ) ?: '—' ); ?></td>
    787                                                                         <td><code><?php echo esc_html( $p['source'] ); ?></code></td>
     1662                                                                        <td><code><?php echo esc_html( $p['item_group_id'] ?: '—' ); ?></code></td>
     1663                                                                        <td><?php echo esc_html( $p['color'] ?: '—' ); ?></td>
     1664                                                                        <td><?php echo esc_html( $p['size'] ?: '—' ); ?></td>
     1665                                                                        <td><?php echo esc_html( $p['material'] ?: '—' ); ?></td>
     1666                                                                        <td><small><?php echo esc_html( ! empty( $label_summary ) ? implode( ' | ', $label_summary ) : '—' ); ?></small></td>
     1667                                                                        <td><a href="?page=wipf-settings&tab=diagnostic&inspect_id=<?php echo esc_attr( $p['id'] ); ?>" class="button button-small"><?php echo esc_html__( 'Inspect', 'world-inspire-product-feed-for-google-shopping' ); ?></a></td>
    7881668                                                                </tr>
    7891669                                                        <?php endforeach; ?>
  • world-inspire-product-feed-for-google-shopping/trunk/readme.txt

    r3730435 r3732072  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.0
     7Stable tag: 1.1.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2121* **Multi-Engine eCommerce Compatibility**: Native auto-detection and data extraction for both **SureCart** (`sc_product`) and **WooCommerce** (`product`). Zero fatal errors if either or neither is active.
    2222* **Google Merchant Center Specification**: Built according to official Google RSS 2.0 standards with all essential namespace attributes: `<g:id>`, `<g:title>`, `<g:description>`, `<g:link>`, `<g:image_link>`, `<g:availability>`, `<g:price>`, `<g:brand>`, `<g:condition>`, `<g:identifier_exists>`, `<g:google_product_category>`, `<g:color>`, `<g:age_group>`, and `<g:gender>`.
     23* **Custom Labels 0–4 for Google Ads / PMax**: Full support for `<g:custom_label_0>` through `<g:custom_label_4>` with rules for price tiers (< $25, $25-$50, $50-$100, $100+), inventory status, category mapping, clearance/promotion tag keywords, and custom post meta fields.
     24* **Variant Grouping (`<g:item_group_id>`)**: Automatically expands WooCommerce variable products (`product_variation`) into individual variant items linked by parent `<g:item_group_id>`, enabling swatch selection in Google Shopping results.
     25* **Variant Attribute Optimization**: Smart extraction for `<g:size>`, `<g:material>` (Gold, Silver, Leather, Cotton, Gemstones, etc.), and `<g:pattern>` (Solid, Striped, Floral, Beaded) directly from attributes or intelligent NLP heuristics.
    2326* **Multi-Country Shipping Generator**: Automatically embeds multi-destination `<g:shipping>` nodes (defaulting to 8 core global markets: US, CA, UK, JP, SG, HK, AE, MX with custom shipping service names and rates).
    24 * **Intelligent Color & Demographic Extraction**: Automatically maps jewelry, apparel, gemstone, and accessory titles/descriptions into recognized Google taxonomy colors (over 35 shades) and adult/unisex demographics.
     27* **Live Diagnostics & Product Inspector Simulator**: Test and audit any product ID directly in WordPress Admin to verify extracted attributes, custom labels matching reasons, and preview raw `<item>` XML snippets.
    2528* **Automatic Pet Category Mapping**: Heuristically detects pet collars, harnesses, and animal accessories and maps them to `Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses`.
    2629* **Trustmate Review Catalog Sync**: Dedicated `<products>` XML feed format ready for instant connection in your Trustmate merchant dashboard.
    … …  
    3942
    4043= Does this plugin work with SureCart? =
    41 Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, and product galleries.
     44Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, collections, tags, and product galleries.
    4245
    4346= Does this plugin work with WooCommerce? =
    44 Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products.
     47Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products and product variations with `<g:item_group_id>`.
     48
     49= How do Custom Labels (0-4) work? =
     50Navigate to **Settings > Product Feeds > Google Shopping Rules & Custom Labels**. You can configure rules for each slot: Price Range Tiers, Inventory Status, Product Category, Tag/Keyword Matching (e.g. `clearance:Clearance, sale:On Sale, bestseller:Best Seller`), or Custom Post Meta fields.
    4551
    4652= Why does this plugin use extension-free REST URLs? =
    … …  
    5359
    54601. Feed URLs & Health Dashboard: Dynamic REST API endpoints and real-time catalog status.
    55 2. Google Shopping Rules: Global fallback brand defaults, product taxonomies, and NLP color detection.
     612. Google Shopping Rules & Custom Labels: Global fallback brand defaults, product taxonomies, NLP color/size/material detection, and custom labels 0-4.
    56623. Multi-Country Shipping: International destination targeting and custom shipping rates.
    57 4. Live Diagnostics: Catalog extraction verification and Google Merchant Center spec compliance audit.
     634. Live Diagnostics & Product Inspector: Catalog extraction audit, custom label rule matching traces, and raw XML simulator.
    5864
    5965== Changelog ==
     66
     67= 1.1.0 =
     68* Added Google Shopping Custom Labels (custom_label_0 to custom_label_4) with configurable rules: Price Tiers, Inventory Status, Category Mapping, Promotional Tag Keywords, and Custom Post Meta.
     69* Added WooCommerce variable product expansion with `<g:item_group_id>` linking parent and variant products.
     70* Added smart attribute extraction for `<g:size>`, `<g:material>`, and `<g:pattern>` with jewelry/apparel heuristic engines.
     71* Added Live Product Inspector & XML Simulator to Diagnostics tab with rule matching explanation traces.
     72* Enforced Google Shopping 100-character maximum length limit and XML entity escaping on all custom labels.
     73* Added 87 zero-dependency unit and mock E2E integration tests passing 100% green.
    6074
    6175= 1.0.0 =
  • world-inspire-product-feed-for-google-shopping/trunk/world-inspire-product-feed-for-google-shopping.php

    r3730219 r3732072  
    33 * Plugin Name: World Inspire Product Feed for Google Shopping
    44 * Plugin URI: https://wordpress.org/plugins/world-inspire-product-feed-for-google-shopping/
    5  * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color and category detection, and anti-cache streaming architecture.
    6  * Version: 1.0.0
     5 * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color/size/material detection, custom labels (0-4), variant item_group_id grouping, and anti-cache streaming architecture.
     6 * Version: 1.1.0
    77 * Author: World Inspire LLC, Vec
    88 * Author URI: https://worldinspirelab.com
    … …  
    1818}
    1919
    20 define( 'WIPF_VERSION', '1.0.0' );
     20define( 'WIPF_VERSION', '1.1.0' );
    2121define( 'WIPF_FILE', __FILE__ );
    2222define( 'WIPF_PATH', plugin_dir_path( __FILE__ ) );
    … …  
    3131function wipf_get_default_settings() {
    3232        return array(
    33                 'brand_name'             => get_bloginfo( 'name' ),
    34                 'default_category'       => 'Apparel & Accessories > Jewelry',
    35                 'default_age_group'      => 'adult',
    36                 'default_gender'         => 'unisex',
    37                 'default_condition'      => 'new',
    38                 'identifier_exists'      => 'no',
    39                 'auto_detect_color'      => 'yes',
    40                 'shipping_countries'     => 'US,CA,GB,JP,SG,HK,AE,MX',
    41                 'shipping_service_name'  => 'Standard Free Shipping',
    42                 'shipping_cost'          => '0.00',
    43                 'shipping_currency'      => 'USD',
    44                 'enable_google_feed'     => 'yes',
    45                 'enable_trustmate_feed'  => 'yes',
    46                 'items_per_feed'         => 500,
     33                'brand_name'              => get_bloginfo( 'name' ),
     34                'default_category'        => 'Apparel & Accessories > Jewelry',
     35                'default_age_group'       => 'adult',
     36                'default_gender'          => 'unisex',
     37                'default_condition'       => 'new',
     38                'identifier_exists'       => 'no',
     39                'auto_detect_color'       => 'yes',
     40                'auto_detect_material'    => 'yes',
     41                'auto_detect_size'        => 'yes',
     42                'auto_detect_pattern'     => 'yes',
     43                'shipping_countries'      => 'US,CA,GB,JP,SG,HK,AE,MX',
     44                'shipping_service_name'   => 'Standard Free Shipping',
     45                'shipping_cost'           => '0.00',
     46                'shipping_currency'       => 'USD',
     47                'enable_google_feed'      => 'yes',
     48                'enable_trustmate_feed'   => 'yes',
     49                'items_per_feed'          => 500,
     50                'enable_custom_labels'    => 'yes',
     51                'custom_label_0_type'     => 'price_tier',
     52                'custom_label_0_value'    => '25,50,100',
     53                'custom_label_0_fallback' => 'Standard',
     54                'custom_label_1_type'     => 'inventory',
     55                'custom_label_1_value'    => '',
     56                'custom_label_1_fallback' => 'Out of Stock',
     57                'custom_label_2_type'     => 'category',
     58                'custom_label_2_value'    => '',
     59                'custom_label_2_fallback' => 'Jewelry',
     60                'custom_label_3_type'     => 'tag_keywords',
     61                'custom_label_3_value'    => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller, new:New Arrival',
     62                'custom_label_3_fallback' => 'Regular',
     63                'custom_label_4_type'     => 'static',
     64                'custom_label_4_value'    => 'World Inspire Catalog',
     65                'custom_label_4_fallback' => '',
    4766        );
    4867}
    … …  
    7897        // Priority compound phrases checked first
    7998        $compounds = array(
    80                 'rose gold'  => 'Rose Gold',
    81                 'tiger eye'  => 'Brown',
     99                'rose gold'    => 'Rose Gold',
     100                'tiger eye'    => 'Brown',
    82101                'lapis lazuli' => 'Blue',
    83102        );
    … …  
    154173
    155174/**
     175 * Intelligent material extraction heuristic.
     176 *
     177 * @param string $title Product title.
     178 * @param string $desc  Product description.
     179 * @return string Extracted material name or empty.
     180 */
     181function wipf_detect_material( $title, $desc = '' ) {
     182        $text = strtolower( $title . ' ' . $desc );
     183
     184        // Priority compound phrases checked first
     185        $compounds = array(
     186                'sterling silver' => 'Sterling Silver',
     187                '925 silver'      => 'Sterling Silver',
     188                'rose gold'       => 'Rose Gold',
     189                'white gold'      => 'White Gold',
     190                'yellow gold'     => 'Yellow Gold',
     191                '14k gold'        => '14K Gold',
     192                '18k gold'        => '18K Gold',
     193                '24k gold'        => '24K Gold',
     194                'stainless steel' => 'Stainless Steel',
     195                'genuine leather' => 'Leather',
     196                'faux leather'    => 'Faux Leather',
     197                'vegan leather'   => 'Faux Leather',
     198                'rose quartz'     => 'Rose Quartz',
     199                'lapis lazuli'    => 'Lapis Lazuli',
     200                'cubic zirconia'  => 'Cubic Zirconia',
     201        );
     202
     203        $detected = array();
     204        foreach ( $compounds as $phrase => $mat_name ) {
     205                if ( false !== strpos( $text, $phrase ) ) {
     206                        if ( ! in_array( $mat_name, $detected, true ) ) {
     207                                $detected[] = $mat_name;
     208                        }
     209                        $text = str_replace( $phrase, '', $text );
     210                }
     211        }
     212
     213        $single_map = array(
     214                'gold'       => 'Gold',
     215                'silver'     => 'Silver',
     216                'platinum'   => 'Platinum',
     217                'titanium'   => 'Titanium',
     218                'brass'      => 'Brass',
     219                'copper'     => 'Copper',
     220                'bronze'     => 'Bronze',
     221                'steel'      => 'Steel',
     222                'diamond'    => 'Diamond',
     223                'pearl'      => 'Pearl',
     224                'amethyst'   => 'Amethyst',
     225                'quartz'     => 'Quartz',
     226                'emerald'    => 'Emerald',
     227                'ruby'       => 'Ruby',
     228                'sapphire'   => 'Sapphire',
     229                'jade'       => 'Jade',
     230                'turquoise'  => 'Turquoise',
     231                'obsidian'   => 'Obsidian',
     232                'onyx'       => 'Onyx',
     233                'opal'       => 'Opal',
     234                'moonstone'  => 'Moonstone',
     235                'topaz'      => 'Topaz',
     236                'garnet'     => 'Garnet',
     237                'tourmaline' => 'Tourmaline',
     238                'amber'      => 'Amber',
     239                'carnelian'  => 'Carnelian',
     240                'citrine'    => 'Citrine',
     241                'crystal'    => 'Crystal',
     242                'leather'    => 'Leather',
     243                'silk'       => 'Silk',
     244                'velvet'     => 'Velvet',
     245                'cotton'     => 'Cotton',
     246                'linen'      => 'Linen',
     247                'wool'       => 'Wool',
     248                'cashmere'   => 'Cashmere',
     249                'satin'      => 'Satin',
     250                'polyester'  => 'Polyester',
     251                'canvas'     => 'Canvas',
     252                'denim'      => 'Denim',
     253                'ceramic'    => 'Ceramic',
     254                'glass'      => 'Glass',
     255                'wood'       => 'Wood',
     256                'silicone'   => 'Silicone',
     257                'resin'      => 'Resin',
     258        );
     259
     260        foreach ( $single_map as $needle => $mat_name ) {
     261                if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) {
     262                        if ( ! in_array( $mat_name, $detected, true ) ) {
     263                                $detected[] = $mat_name;
     264                        }
     265                }
     266        }
     267
     268        if ( count( $detected ) > 2 ) {
     269                return 'Mixed';
     270        } elseif ( ! empty( $detected ) ) {
     271                return implode( '/', $detected );
     272        }
     273
     274        return '';
     275}
     276
     277/**
     278 * Intelligent pattern extraction heuristic.
     279 *
     280 * @param string $title Product title.
     281 * @param string $desc  Product description.
     282 * @return string Extracted pattern or empty.
     283 */
     284function wipf_detect_pattern( $title, $desc = '' ) {
     285        $text = strtolower( $title . ' ' . $desc );
     286
     287        $patterns = array(
     288                'polka dot'    => 'Polka Dot',
     289                'polka dots'   => 'Polka Dot',
     290                'animal print' => 'Animal Print',
     291                'striped'      => 'Striped',
     292                'stripes'      => 'Striped',
     293                'stripe'       => 'Striped',
     294                'floral'       => 'Floral',
     295                'flower'       => 'Floral',
     296                'geometric'    => 'Geometric',
     297                'beaded'       => 'Beaded',
     298                'braided'      => 'Braided',
     299                'plaid'        => 'Plaid',
     300                'checkered'    => 'Checkered',
     301                'leopard'      => 'Animal Print',
     302                'cheetah'      => 'Animal Print',
     303                'zebra'        => 'Animal Print',
     304                'camouflage'   => 'Camouflage',
     305                'camo'         => 'Camouflage',
     306                'marble'       => 'Marble',
     307                'gradient'     => 'Gradient',
     308                'solid'        => 'Solid',
     309        );
     310
     311        foreach ( $patterns as $needle => $pat_name ) {
     312                if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) {
     313                        return $pat_name;
     314                }
     315        }
     316
     317        return '';
     318}
     319
     320/**
     321 * Intelligent size extraction heuristic.
     322 *
     323 * @param string $title Product title.
     324 * @param string $desc  Product description.
     325 * @return string Extracted size or empty.
     326 */
     327function wipf_detect_size( $title, $desc = '' ) {
     328        $text = $title . ' ' . $desc;
     329
     330        // 1. One Size / OS
     331        if ( preg_match( '/\b(one\s*size|osfm|osfa)\b/i', $text ) ) {
     332                return 'One Size';
     333        }
     334
     335        // 2. Ring or shoe sizes: "Size 7", "Size 7.5", "US 8"
     336        if ( preg_match( '/\b(?:size|us|eu)\s*([0-9]+(?:\.[0-9]+)?)\b/i', $text, $matches ) ) {
     337                return $matches[1];
     338        }
     339
     340        // 3. Dimensional bead / diameter sizes: "8mm", "10mm", "12mm"
     341        if ( preg_match( '/\b([0-9]+(?:\.[0-9]+)?\s*(?:mm|cm|in|inch|inches))\b/i', $text, $matches ) ) {
     342                return trim( $matches[1] );
     343        }
     344
     345        // 4. Apparel sizes: XS, S, M, L, XL, XXL, 2XL, 3XL
     346        if ( preg_match( '/\b(XS|S|M|L|XL|XXL|2XL|3XL)\b/', $text, $matches ) ) {
     347                return $matches[1];
     348        }
     349
     350        return '';
     351}
     352
     353/**
     354 * Evaluates a Google Shopping custom label (0-4) according to configured rules.
     355 *
     356 * @param int   $slot    Custom label index (0 to 4).
     357 * @param array $product Normalized product data.
     358 * @param array $config  Plugin settings.
     359 * @return array Array with keys 'value', 'reason', 'type'.
     360 */
     361function wipf_evaluate_custom_label( $slot, $product, $config ) {
     362        $slot = absint( $slot );
     363        if ( $slot < 0 || $slot > 4 ) {
     364                return array( 'value' => '', 'reason' => 'Invalid slot', 'type' => 'disabled' );
     365        }
     366
     367        $type     = ! empty( $config["custom_label_{$slot}_type"] ) ? $config["custom_label_{$slot}_type"] : 'disabled';
     368        $rule_val = ! empty( $config["custom_label_{$slot}_value"] ) ? trim( $config["custom_label_{$slot}_value"] ) : '';
     369        $fallback = ! empty( $config["custom_label_{$slot}_fallback"] ) ? trim( $config["custom_label_{$slot}_fallback"] ) : '';
     370
     371        $result = '';
     372        $reason = '';
     373
     374        switch ( $type ) {
     375                case 'disabled':
     376                        return array(
     377                                'value'  => '',
     378                                'reason' => 'Slot disabled',
     379                                'type'   => 'disabled',
     380                        );
     381
     382                case 'static':
     383                        $result = $rule_val;
     384                        $reason = 'Static configured value: ' . $rule_val;
     385                        break;
     386
     387                case 'price_tier':
     388                        $price = isset( $product['price'] ) ? (float) $product['price'] : 0.0;
     389                        $curr  = ! empty( $product['currency'] ) ? $product['currency'] : ( $config['shipping_currency'] ?? 'USD' );
     390
     391                        // Check custom thresholds e.g. "50" or "30,80" or "25,50,100"
     392                        $thresholds = array( 25.0, 50.0, 100.0 );
     393                        if ( ! empty( $rule_val ) ) {
     394                                $parsed = array();
     395                                $raw_parts = explode( ',', $rule_val );
     396                                foreach ( $raw_parts as $rp ) {
     397                                        $fv = (float) trim( $rp );
     398                                        if ( $fv > 0 ) {
     399                                                $parsed[] = $fv;
     400                                        }
     401                                }
     402                                if ( ! empty( $parsed ) ) {
     403                                        sort( $parsed );
     404                                        $thresholds = array_values( array_unique( $parsed ) );
     405                                }
     406                        }
     407
     408                        $count = count( $thresholds );
     409                        if ( $price < $thresholds[0] ) {
     410                                $result = 'Under ' . (int) $thresholds[0];
     411                                $reason = sprintf( 'Price %0.2f %s < %0.2f', $price, $curr, $thresholds[0] );
     412                        } elseif ( $price >= $thresholds[ $count - 1 ] ) {
     413                                $result = (int) $thresholds[ $count - 1 ] . ' and Above';
     414                                $reason = sprintf( 'Price %0.2f %s >= %0.2f', $price, $curr, $thresholds[ $count - 1 ] );
     415                        } else {
     416                                for ( $ti = 0; $ti < $count - 1; $ti++ ) {
     417                                        if ( $price >= $thresholds[ $ti ] && $price < $thresholds[ $ti + 1 ] ) {
     418                                                $result = (int) $thresholds[ $ti ] . ' to ' . (int) $thresholds[ $ti + 1 ];
     419                                                $reason = sprintf( 'Price %0.2f %s between %0.2f and %0.2f', $price, $curr, $thresholds[ $ti ], $thresholds[ $ti + 1 ] );
     420                                                break;
     421                                        }
     422                                }
     423                        }
     424                        break;
     425
     426                case 'inventory':
     427                        $status = ! empty( $product['availability'] ) ? $product['availability'] : 'in_stock';
     428                        $in_label  = 'In Stock';
     429                        $out_label = 'Out of Stock';
     430                        if ( ! empty( $rule_val ) && false !== strpos( $rule_val, ':' ) ) {
     431                                $pairs = explode( ',', $rule_val );
     432                                foreach ( $pairs as $pair ) {
     433                                        $p = array_map( 'trim', explode( ':', $pair, 2 ) );
     434                                        if ( count( $p ) === 2 ) {
     435                                                if ( 'in_stock' === $p[0] ) {
     436                                                        $in_label = $p[1];
     437                                                } elseif ( 'out_of_stock' === $p[0] ) {
     438                                                        $out_label = $p[1];
     439                                                }
     440                                        }
     441                                }
     442                        }
     443
     444                        if ( 'out_of_stock' === $status ) {
     445                                $result = $out_label;
     446                                $reason = 'Inventory status is out_of_stock';
     447                        } else {
     448                                $result = $in_label;
     449                                $reason = 'Inventory status is in_stock';
     450                        }
     451                        break;
     452
     453                case 'category':
     454                        if ( ! empty( $product['categories'] ) && is_array( $product['categories'] ) ) {
     455                                $result = reset( $product['categories'] );
     456                                $reason = 'Derived from primary category: ' . $result;
     457                        } elseif ( ! empty( $product['category'] ) ) {
     458                                $result = $product['category'];
     459                                $reason = 'Derived from product category: ' . $result;
     460                        } else {
     461                                $result = ! empty( $config['default_category'] ) ? $config['default_category'] : '';
     462                                $reason = 'Fallback to default category';
     463                        }
     464                        break;
     465
     466                case 'tag_keywords':
     467                        $tags_list = ! empty( $product['tags'] ) && is_array( $product['tags'] ) ? $product['tags'] : array();
     468                        $text_to_search = strtolower( ( $product['title'] ?? '' ) . ' ' . implode( ' ', $tags_list ) );
     469
     470                        if ( ! empty( $rule_val ) ) {
     471                                $pairs = explode( ',', $rule_val );
     472                                foreach ( $pairs as $pair ) {
     473                                        $parts = array_map( 'trim', explode( ':', $pair, 2 ) );
     474                                        $keyword = strtolower( $parts[0] );
     475                                        $label   = isset( $parts[1] ) && '' !== $parts[1] ? $parts[1] : ucwords( $parts[0] );
     476
     477                                        if ( '' !== $keyword && false !== strpos( $text_to_search, $keyword ) ) {
     478                                                $result = $label;
     479                                                $reason = "Matched keyword '{$keyword}' in title or tags";
     480                                                break;
     481                                        }
     482                                }
     483                        }
     484
     485                        if ( empty( $result ) ) {
     486                                $reason = 'No matching tag or keyword found';
     487                        }
     488                        break;
     489
     490                case 'custom_meta':
     491                        $meta_key = $rule_val;
     492                        if ( ! empty( $meta_key ) && ! empty( $product['id'] ) && function_exists( 'get_post_meta' ) ) {
     493                                $meta_val = get_post_meta( $product['id'], $meta_key, true );
     494                                if ( empty( $meta_val ) && ! empty( $product['item_group_id'] ) ) {
     495                                        $meta_val = get_post_meta( $product['item_group_id'], $meta_key, true );
     496                                }
     497                                if ( ! empty( $meta_val ) && is_scalar( $meta_val ) ) {
     498                                        $result = (string) $meta_val;
     499                                        $reason = "Retrieved from post meta '{$meta_key}'";
     500                                } else {
     501                                        $reason = "Post meta '{$meta_key}' is empty";
     502                                }
     503                        } else {
     504                                $reason = 'Custom meta key not specified';
     505                        }
     506                        break;
     507
     508                default:
     509                        $reason = 'Unknown rule type: ' . $type;
     510                        break;
     511        }
     512
     513        // Apply fallback if result is empty
     514        if ( '' === $result && '' !== $fallback ) {
     515                $result = $fallback;
     516                $reason .= ' (Fallback applied: ' . $fallback . ')';
     517        }
     518
     519        // Google Shopping max length for custom_label_0-4 is 100 characters
     520        if ( '' !== $result ) {
     521                $clean_result = sanitize_text_field( $result );
     522                if ( function_exists( 'mb_strlen' ) ) {
     523                        if ( mb_strlen( $clean_result, 'UTF-8' ) > 100 ) {
     524                                $clean_result = mb_substr( $clean_result, 0, 100, 'UTF-8' );
     525                        }
     526                } else {
     527                        $clean_result = substr( $clean_result, 0, 100 );
     528                }
     529                $result = $clean_result;
     530        }
     531
     532        return array(
     533                'value'  => $result,
     534                'reason' => $reason,
     535                'type'   => $type,
     536        );
     537}
     538
     539/**
    156540 * Safe XML entity escaper.
    157541 *
    … …  
    165549/**
    166550 * Fetches normalized product catalog from active e-commerce platforms.
    167  * Supports SureCart and WooCommerce seamlessly.
     551 * Supports SureCart and WooCommerce seamlessly including product variations.
    168552 *
    169553 * @param int $limit Maximum number of products to return.
    … …  
    176560                $limit = 500;
    177561        }
     562
     563        $settings = wipf_get_settings();
    178564
    179565        // 1. Check SureCart products (sc_product)
    … …  
    239625                                }
    240626
     627                                // Categories & Tags
     628                                $categories = array();
     629                                if ( function_exists( 'wp_get_post_terms' ) ) {
     630                                        $sc_terms = wp_get_post_terms( $post_id, 'sc_collection', array( 'fields' => 'names' ) );
     631                                        if ( is_array( $sc_terms ) ) {
     632                                                $categories = $sc_terms;
     633                                        }
     634                                }
     635
     636                                $tags = array();
     637                                if ( function_exists( 'wp_get_post_terms' ) ) {
     638                                        $sc_tag_terms = wp_get_post_terms( $post_id, 'sc_tag', array( 'fields' => 'names' ) );
     639                                        if ( is_array( $sc_tag_terms ) ) {
     640                                                $tags = $sc_tag_terms;
     641                                        }
     642                                }
     643
     644                                // Variant attributes
     645                                $parent_id = get_post_meta( $post_id, 'sc_parent_id', true );
     646
     647                                $sc_color = get_post_meta( $post_id, 'sc_color', true );
     648                                if ( empty( $sc_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) {
     649                                        $sc_color = wipf_detect_color( $title, $content );
     650                                }
     651
     652                                $sc_size = get_post_meta( $post_id, 'sc_size', true );
     653                                if ( empty( $sc_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) {
     654                                        $sc_size = wipf_detect_size( $title, $content );
     655                                }
     656
     657                                $sc_mat = get_post_meta( $post_id, 'sc_material', true );
     658                                if ( empty( $sc_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) {
     659                                        $sc_mat = wipf_detect_material( $title, $content );
     660                                }
     661
     662                                $sc_pat = get_post_meta( $post_id, 'sc_pattern', true );
     663                                if ( empty( $sc_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) {
     664                                        $sc_pat = wipf_detect_pattern( $title, $content );
     665                                }
     666
    241667                                $products[] = array(
    242668                                        'id'             => $post_id,
     669                                        'item_group_id'  => ! empty( $parent_id ) ? (string) $parent_id : '',
    243670                                        'title'          => $title,
    244671                                        'description'    => $content,
    … …  
    250677                                        'availability'   => $inv_status,
    251678                                        'source'         => 'surecart',
     679                                        'color'          => $sc_color,
     680                                        'size'           => $sc_size,
     681                                        'material'       => $sc_mat,
     682                                        'pattern'        => $sc_pat,
     683                                        'categories'     => $categories,
     684                                        'tags'           => $tags,
    252685                                );
     686
     687                                if ( count( $products ) >= $limit ) {
     688                                        break;
     689                                }
    253690                        }
    254691                        wp_reset_postdata();
    … …  
    257694
    258695        // 2. Check WooCommerce products (product)
    259         if ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ) {
     696        if ( ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ) && count( $products ) < $limit ) {
    260697                $wc_args = array(
    261698                        'post_type'      => 'product',
    262699                        'post_status'    => 'publish',
    263                         'posts_per_page' => $limit,
     700                        'posts_per_page' => $limit - count( $products ),
    264701                        'orderby'        => 'ID',
    265702                        'order'          => 'DESC',
    … …  
    284721                                $inv_status = 'in_stock';
    285722
    286                                 if ( function_exists( 'wc_get_product' ) ) {
    287                                         $wc_prod = wc_get_product( $post_id );
    288                                         if ( $wc_prod ) {
    289                                                 $price = $wc_prod->get_price();
    290                                                 $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock';
     723                                // Categories & Tags
     724                                $categories = array();
     725                                if ( function_exists( 'wp_get_post_terms' ) ) {
     726                                        $wc_terms = wp_get_post_terms( $post_id, 'product_cat', array( 'fields' => 'names' ) );
     727                                        if ( is_array( $wc_terms ) ) {
     728                                                $categories = $wc_terms;
    291729                                        }
    292                                 } else {
    293                                         $raw_price = get_post_meta( $post_id, '_price', true );
    294                                         if ( is_numeric( $raw_price ) ) {
    295                                                 $price = (float) $raw_price;
     730                                }
     731
     732                                $tags = array();
     733                                if ( function_exists( 'wp_get_post_terms' ) ) {
     734                                        $wc_tag_terms = wp_get_post_terms( $post_id, 'product_tag', array( 'fields' => 'names' ) );
     735                                        if ( is_array( $wc_tag_terms ) ) {
     736                                                $tags = $wc_tag_terms;
    296737                                        }
    297738                                }
    … …  
    314755                                }
    315756
     757                                // Check WooCommerce Product object & Variable product handling
     758                                $wc_prod = function_exists( 'wc_get_product' ) ? wc_get_product( $post_id ) : null;
     759                                if ( $wc_prod ) {
     760                                        // Variable products: expand into variations with item_group_id
     761                                        if ( method_exists( $wc_prod, 'is_type' ) && $wc_prod->is_type( 'variable' ) ) {
     762                                                $variation_ids = method_exists( $wc_prod, 'get_children' ) ? $wc_prod->get_children() : array();
     763                                                if ( ! empty( $variation_ids ) ) {
     764                                                        foreach ( $variation_ids as $var_id ) {
     765                                                                $var_obj = wc_get_product( $var_id );
     766                                                                if ( ! $var_obj ) {
     767                                                                        continue;
     768                                                                }
     769
     770                                                                $var_price = $var_obj->get_price();
     771                                                                $var_inv   = $var_obj->is_in_stock() ? 'in_stock' : 'out_of_stock';
     772                                                                $var_link  = $var_obj->get_permalink() ?: $permalink;
     773
     774                                                                $var_img_id = $var_obj->get_image_id();
     775                                                                $var_image  = $var_img_id ? wp_get_attachment_url( $var_img_id ) : $image_url;
     776
     777                                                                // Variation attributes
     778                                                                $var_color = $var_obj->get_attribute( 'pa_color' ) ?: $var_obj->get_attribute( 'color' );
     779                                                                if ( empty( $var_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) {
     780                                                                        $var_color = wipf_detect_color( $var_obj->get_name() . ' ' . $title, $content );
     781                                                                }
     782
     783                                                                $var_size = $var_obj->get_attribute( 'pa_size' ) ?: $var_obj->get_attribute( 'size' );
     784                                                                if ( empty( $var_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) {
     785                                                                        $var_size = wipf_detect_size( $var_obj->get_name() . ' ' . $title, $content );
     786                                                                }
     787
     788                                                                $var_mat = $var_obj->get_attribute( 'pa_material' ) ?: $var_obj->get_attribute( 'material' );
     789                                                                if ( empty( $var_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) {
     790                                                                        $var_mat = wipf_detect_material( $var_obj->get_name() . ' ' . $title, $content );
     791                                                                }
     792
     793                                                                $var_pat = $var_obj->get_attribute( 'pa_pattern' ) ?: $var_obj->get_attribute( 'pattern' );
     794                                                                if ( empty( $var_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) {
     795                                                                        $var_pat = wipf_detect_pattern( $var_obj->get_name() . ' ' . $title, $content );
     796                                                                }
     797
     798                                                                $products[] = array(
     799                                                                        'id'             => $var_id,
     800                                                                        'item_group_id'  => (string) $post_id,
     801                                                                        'title'          => $var_obj->get_name() ?: $title,
     802                                                                        'description'    => $content,
     803                                                                        'permalink'      => $var_link,
     804                                                                        'image_url'      => $var_image,
     805                                                                        'gallery_images' => $gallery_images,
     806                                                                        'price'          => number_format( (float) $var_price, 2, '.', '' ),
     807                                                                        'currency'       => $currency,
     808                                                                        'availability'   => $var_inv,
     809                                                                        'source'         => 'woocommerce',
     810                                                                        'color'          => $var_color,
     811                                                                        'size'           => $var_size,
     812                                                                        'material'       => $var_mat,
     813                                                                        'pattern'        => $var_pat,
     814                                                                        'categories'     => $categories,
     815                                                                        'tags'           => $tags,
     816                                                                );
     817
     818                                                                if ( count( $products ) >= $limit ) {
     819                                                                        break 2;
     820                                                                }
     821                                                        }
     822                                                        continue; // Variations added, skip parent product
     823                                                }
     824                                        }
     825
     826                                        // Simple product
     827                                        $price = $wc_prod->get_price();
     828                                        $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock';
     829                                } else {
     830                                        $raw_price = get_post_meta( $post_id, '_price', true );
     831                                        if ( is_numeric( $raw_price ) ) {
     832                                                $price = (float) $raw_price;
     833                                        }
     834                                }
     835
     836                                // Simple product attributes
     837                                $prod_color = '';
     838                                $prod_size  = '';
     839                                $prod_mat   = '';
     840                                $prod_pat   = '';
     841
     842                                if ( $wc_prod && method_exists( $wc_prod, 'get_attribute' ) ) {
     843                                        $prod_color = $wc_prod->get_attribute( 'pa_color' ) ?: $wc_prod->get_attribute( 'color' );
     844                                        $prod_size  = $wc_prod->get_attribute( 'pa_size' ) ?: $wc_prod->get_attribute( 'size' );
     845                                        $prod_mat   = $wc_prod->get_attribute( 'pa_material' ) ?: $wc_prod->get_attribute( 'material' );
     846                                        $prod_pat   = $wc_prod->get_attribute( 'pa_pattern' ) ?: $wc_prod->get_attribute( 'pattern' );
     847                                }
     848
     849                                if ( empty( $prod_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) {
     850                                        $prod_color = wipf_detect_color( $title, $content );
     851                                }
     852                                if ( empty( $prod_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) {
     853                                        $prod_size = wipf_detect_size( $title, $content );
     854                                }
     855                                if ( empty( $prod_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) {
     856                                        $prod_mat = wipf_detect_material( $title, $content );
     857                                }
     858                                if ( empty( $prod_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) {
     859                                        $prod_pat = wipf_detect_pattern( $title, $content );
     860                                }
     861
    316862                                $products[] = array(
    317863                                        'id'             => $post_id,
     864                                        'item_group_id'  => '',
    318865                                        'title'          => $title,
    319866                                        'description'    => $content,
    … …  
    325872                                        'availability'   => $inv_status,
    326873                                        'source'         => 'woocommerce',
     874                                        'color'          => $prod_color,
     875                                        'size'           => $prod_size,
     876                                        'material'       => $prod_mat,
     877                                        'pattern'        => $prod_pat,
     878                                        'categories'     => $categories,
     879                                        'tags'           => $tags,
    327880                                );
     881
     882                                if ( count( $products ) >= $limit ) {
     883                                        break;
     884                                }
    328885                        }
    329886                        wp_reset_postdata();
    … …  
    353910        $ship_cost       = ! empty( $config['shipping_cost'] ) ? $config['shipping_cost'] : '0.00';
    354911        $ship_curr       = ! empty( $config['shipping_currency'] ) ? $config['shipping_currency'] : 'USD';
     912        $enable_labels   = ( 'no' !== ( $config['enable_custom_labels'] ?? 'yes' ) );
    355913
    356914        $xml  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    … …  
    367925                $price_val   = $item['price'];
    368926                $curr_val    = ! empty( $item['currency'] ) ? $item['currency'] : $ship_curr;
    369                 $item_color  = $auto_color ? wipf_detect_color( $title, $desc ) : '';
    370 
    371                 // Pet classification heuristic
     927
     928                // Color priority: item attribute > heuristic
     929                $item_color  = ! empty( $item['color'] ) ? $item['color'] : ( $auto_color ? wipf_detect_color( $title, $desc ) : '' );
     930
     931                // Category classification heuristic
    372932                $category = $def_category;
     933                if ( ! empty( $item['categories'] ) && is_array( $item['categories'] ) ) {
     934                        $category = implode( ' > ', $item['categories'] );
     935                }
    373936                if ( false !== stripos( $title, 'collar' ) || false !== stripos( $title, 'pet' ) || false !== stripos( $title, 'dog' ) || false !== stripos( $title, 'cat' ) ) {
    374937                        $category = 'Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses';
    … …  
    377940                $xml .= "    <item>\n";
    378941                $xml .= '      <g:id>' . wipf_xml_escape( $item['id'] ) . "</g:id>\n";
     942
     943                // Variant item_group_id grouping
     944                if ( ! empty( $item['item_group_id'] ) ) {
     945                        $xml .= '      <g:item_group_id>' . wipf_xml_escape( $item['item_group_id'] ) . "</g:item_group_id>\n";
     946                }
     947
    379948                $xml .= '      <g:title>' . wipf_xml_escape( $title ) . "</g:title>\n";
    380949                $xml .= '      <g:description>' . wipf_xml_escape( $desc ) . "</g:description>\n";
    … …  
    400969                        $xml .= '      <g:color>' . wipf_xml_escape( $item_color ) . "</g:color>\n";
    401970                }
     971                if ( ! empty( $item['size'] ) ) {
     972                        $xml .= '      <g:size>' . wipf_xml_escape( $item['size'] ) . "</g:size>\n";
     973                }
     974                if ( ! empty( $item['material'] ) ) {
     975                        $xml .= '      <g:material>' . wipf_xml_escape( $item['material'] ) . "</g:material>\n";
     976                }
     977                if ( ! empty( $item['pattern'] ) ) {
     978                        $xml .= '      <g:pattern>' . wipf_xml_escape( $item['pattern'] ) . "</g:pattern>\n";
     979                }
     980
    402981                if ( ! empty( $def_age_group ) ) {
    403982                        $xml .= '      <g:age_group>' . wipf_xml_escape( $def_age_group ) . "</g:age_group>\n";
    … …  
    405984                if ( ! empty( $def_gender ) ) {
    406985                        $xml .= '      <g:gender>' . wipf_xml_escape( $def_gender ) . "</g:gender>\n";
     986                }
     987
     988                // Inject Custom Labels (custom_label_0 to custom_label_4)
     989                if ( $enable_labels ) {
     990                        for ( $slot = 0; $slot <= 4; $slot++ ) {
     991                                $label_data = wipf_evaluate_custom_label( $slot, $item, $config );
     992                                if ( ! empty( $label_data['value'] ) ) {
     993                                        $xml .= '      <g:custom_label_' . $slot . '>' . wipf_xml_escape( $label_data['value'] ) . '</g:custom_label_' . $slot . ">\n";
     994                                }
     995                        }
    407996                }
    408997
    … …  
    4631052
    4641053/**
     1054 * Detailed diagnostic inspector for a specific product ID or product data array.
     1055 *
     1056 * @param int|array  $product Product ID or product array.
     1057 * @param array|null $config  Optional configuration override.
     1058 * @return array Diagnostic report.
     1059 */
     1060function wipf_diagnose_product( $product, $config = null ) {
     1061        if ( null === $config ) {
     1062                $config = wipf_get_settings();
     1063        }
     1064
     1065        $item = is_array( $product ) ? $product : null;
     1066        if ( null === $item && is_numeric( $product ) ) {
     1067                $limit = 200;
     1068                $all_prods = wipf_collect_products( $limit );
     1069                foreach ( $all_prods as $p ) {
     1070                        if ( (int) $p['id'] === (int) $product ) {
     1071                                $item = $p;
     1072                                break;
     1073                        }
     1074                }
     1075                if ( null === $item && function_exists( 'get_post' ) ) {
     1076                        $post = get_post( (int) $product );
     1077                        if ( $post && 'publish' === ( $post->post_status ?? '' ) ) {
     1078                                $title = get_the_title( $post );
     1079                                $content = wp_strip_all_tags( get_the_excerpt( $post ) ?: ( get_the_content( null, false, $post ) ?: $title ) );
     1080                                $raw_price = get_post_meta( $post->ID, '_price', true );
     1081                                $price = is_numeric( $raw_price ) ? (float) $raw_price : 0.0;
     1082                                $image = get_the_post_thumbnail_url( $post->ID, 'full' ) ?: '';
     1083
     1084                                $categories = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_cat', array( 'fields' => 'names' ) ) : array();
     1085                                $tags = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_tag', array( 'fields' => 'names' ) ) : array();
     1086
     1087                                $item = array(
     1088                                        'id'             => $post->ID,
     1089                                        'item_group_id'  => '',
     1090                                        'title'          => $title,
     1091                                        'description'    => $content,
     1092                                        'permalink'      => get_permalink( $post->ID ),
     1093                                        'image_url'      => $image,
     1094                                        'gallery_images' => array(),
     1095                                        'price'          => number_format( $price, 2, '.', '' ),
     1096                                        'currency'       => $config['shipping_currency'] ?? 'USD',
     1097                                        'availability'   => 'in_stock',
     1098                                        'source'         => $post->post_type,
     1099                                        'color'          => wipf_detect_color( $title, $content ),
     1100                                        'size'           => wipf_detect_size( $title, $content ),
     1101                                        'material'       => wipf_detect_material( $title, $content ),
     1102                                        'pattern'        => wipf_detect_pattern( $title, $content ),
     1103                                        'categories'     => is_array( $categories ) ? $categories : array(),
     1104                                        'tags'           => is_array( $tags ) ? $tags : array(),
     1105                                );
     1106                        }
     1107                }
     1108        }
     1109
     1110        if ( empty( $item ) ) {
     1111                return array(
     1112                        'found' => false,
     1113                        'error' => __( 'Product not found or not published in catalog.', 'world-inspire-product-feed-for-google-shopping' ),
     1114                );
     1115        }
     1116
     1117        $labels = array();
     1118        for ( $i = 0; $i <= 4; $i++ ) {
     1119                $labels[ "custom_label_{$i}" ] = wipf_evaluate_custom_label( $i, $item, $config );
     1120        }
     1121
     1122        // Generate XML snippet for just this item
     1123        $xml_doc = wipf_build_google_shopping_xml( array( $item ), $config );
     1124        $xml_snippet = '';
     1125        if ( preg_match( '/<item>.*?<\/item>/s', $xml_doc, $matches ) ) {
     1126                $xml_snippet = trim( $matches[0] );
     1127        }
     1128
     1129        return array(
     1130                'found'       => true,
     1131                'product'     => $item,
     1132                'labels'      => $labels,
     1133                'xml_snippet' => $xml_snippet,
     1134        );
     1135}
     1136
     1137/**
    4651138 * Streams XML document with anti-caching reverse-proxy protection.
    4661139 *
    … …  
    4821155
    4831156/**
    484  * Registers REST API endpoints for Google Shopping and Trustmate.
     1157 * Registers REST API endpoints for Google Shopping, Trustmate, and Status.
    4851158 */
    4861159function wipf_register_rest_routes() {
    … …  
    5251198                                return new WP_REST_Response(
    5261199                                        array(
    527                                                 'enabled'            => true,
    528                                                 'status'             => 'healthy',
    529                                                 'version'            => WIPF_VERSION,
    530                                                 'surecart_detected'  => post_type_exists( 'sc_product' ),
     1200                                                'enabled'              => true,
     1201                                                'status'               => 'healthy',
     1202                                                'version'              => WIPF_VERSION,
     1203                                                'surecart_detected'    => post_type_exists( 'sc_product' ),
    5311204                                                'woocommerce_detected' => ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ),
    532                                                 'sample_count'       => count( $products ),
     1205                                                'sample_count'         => count( $products ),
     1206                                                'custom_labels_active' => ( 'no' !== ( wipf_get_settings()['enable_custom_labels'] ?? 'yes' ) ),
    5331207                                        ),
    5341208                                        200
    … …  
    5731247        $output['identifier_exists']     = ( 'yes' === ( $input['identifier_exists'] ?? 'no' ) ) ? 'yes' : 'no';
    5741248        $output['auto_detect_color']     = ( 'yes' === ( $input['auto_detect_color'] ?? 'yes' ) ) ? 'yes' : 'no';
     1249        $output['auto_detect_material']  = ( 'yes' === ( $input['auto_detect_material'] ?? 'yes' ) ) ? 'yes' : 'no';
     1250        $output['auto_detect_size']      = ( 'yes' === ( $input['auto_detect_size'] ?? 'yes' ) ) ? 'yes' : 'no';
     1251        $output['auto_detect_pattern']   = ( 'yes' === ( $input['auto_detect_pattern'] ?? 'yes' ) ) ? 'yes' : 'no';
    5751252        $output['shipping_countries']    = ! empty( $input['shipping_countries'] ) ? sanitize_text_field( $input['shipping_countries'] ) : $defaults['shipping_countries'];
    5761253        $output['shipping_service_name'] = ! empty( $input['shipping_service_name'] ) ? sanitize_text_field( $input['shipping_service_name'] ) : $defaults['shipping_service_name'];
    … …  
    5801257        $output['enable_trustmate_feed'] = ( 'yes' === ( $input['enable_trustmate_feed'] ?? 'yes' ) ) ? 'yes' : 'no';
    5811258        $output['items_per_feed']        = absint( $input['items_per_feed'] ?? 500 );
     1259
     1260        // Custom Labels (custom_label_0 to custom_label_4)
     1261        $output['enable_custom_labels']  = ( 'yes' === ( $input['enable_custom_labels'] ?? 'yes' ) ) ? 'yes' : 'no';
     1262        $valid_rule_types = array( 'disabled', 'static', 'price_tier', 'inventory', 'category', 'tag_keywords', 'custom_meta' );
     1263
     1264        for ( $i = 0; $i <= 4; $i++ ) {
     1265                $type_key     = "custom_label_{$i}_type";
     1266                $val_key      = "custom_label_{$i}_value";
     1267                $fallback_key = "custom_label_{$i}_fallback";
     1268
     1269                $type = isset( $input[ $type_key ] ) && in_array( $input[ $type_key ], $valid_rule_types, true ) ? sanitize_key( $input[ $type_key ] ) : 'disabled';
     1270                $val  = isset( $input[ $val_key ] ) ? sanitize_text_field( $input[ $val_key ] ) : '';
     1271                $fb   = isset( $input[ $fallback_key ] ) ? sanitize_text_field( $input[ $fallback_key ] ) : '';
     1272
     1273                $output[ $type_key ]     = $type;
     1274                $output[ $val_key ]      = $val;
     1275                $output[ $fallback_key ] = $fb;
     1276        }
    5821277
    5831278        return $output;
    … …  
    6141309        ?>
    6151310        <div class="wrap">
    616                 <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?></h1>
     1311                <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?> <span style="font-size: 14px; background: #2271b1; color: #fff; padding: 2px 8px; border-radius: 4px; vertical-align: middle;">v<?php echo esc_html( WIPF_VERSION ); ?></span></h1>
    6171312                <p class="description">
    6181313                        <?php echo esc_html__( 'Zero-configuration, high-performance real-time XML product feeds engineered for Google Merchant Center and Trustmate review synchronization.', 'world-inspire-product-feed-for-google-shopping' ); ?>
    … …  
    6241319                        </a>
    6251320                        <a href="?page=wipf-settings&tab=google" class="nav-tab <?php echo 'google' === $active_tab ? 'nav-tab-active' : ''; ?>">
    626                                 <?php echo esc_html__( 'Google Shopping Rules', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1321                                <?php echo esc_html__( 'Google Shopping Rules & Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?>
    6271322                        </a>
    6281323                        <a href="?page=wipf-settings&tab=shipping" class="nav-tab <?php echo 'shipping' === $active_tab ? 'nav-tab-active' : ''; ?>">
    … …  
    6301325                        </a>
    6311326                        <a href="?page=wipf-settings&tab=diagnostic" class="nav-tab <?php echo 'diagnostic' === $active_tab ? 'nav-tab-active' : ''; ?>">
    632                                 <?php echo esc_html__( 'Live Diagnostics & Preview', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1327                                <?php echo esc_html__( 'Live Diagnostics & Inspector', 'world-inspire-product-feed-for-google-shopping' ); ?>
    6331328                        </a>
    6341329                </h2>
    6351330
    6361331                <?php if ( 'endpoints' === $active_tab ) : ?>
    637                         <div class="card" style="max-width: 900px; margin-top: 20px; padding: 20px;">
     1332                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
    6381333                                <h3><?php echo esc_html__( 'Authoritative Feed URLs', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    6391334                                <p><?php echo esc_html__( 'Copy these extension-free dynamic REST endpoints into Google Merchant Center and Trustmate. They automatically bypass reverse-proxy static file caches and update in real-time.', 'world-inspire-product-feed-for-google-shopping' ); ?></p>
    … …  
    6641359                        </div>
    6651360                <?php elseif ( 'google' === $active_tab || 'shipping' === $active_tab ) : ?>
    666                         <form method="post" action="options.php" style="max-width: 900px; margin-top: 20px;">
     1361                        <form method="post" action="options.php" style="max-width: 960px; margin-top: 20px;">
    6671362                                <?php
    6681363                                settings_fields( 'wipf_settings_group' );
    6691364                                ?>
    670                                 <div class="card" style="padding: 20px;">
    671                                         <?php if ( 'google' === $active_tab ) : ?>
     1365                                <?php if ( 'google' === $active_tab ) : ?>
     1366                                        <div class="card" style="padding: 20px; margin-bottom: 20px;">
    6721367                                                <h3><?php echo esc_html__( 'Google Shopping Catalog Standards', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    6731368                                                <table class="form-table" role="presentation">
    … …  
    6921387                                                                                <option value="kids" <?php selected( $settings['default_age_group'], 'kids' ); ?>><?php echo esc_html__( 'Kids', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
    6931388                                                                                <option value="toddler" <?php selected( $settings['default_age_group'], 'toddler' ); ?>><?php echo esc_html__( 'Toddler', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1389                                                                                <option value="infant" <?php selected( $settings['default_age_group'], 'infant' ); ?>><?php echo esc_html__( 'Infant', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1390                                                                                <option value="newborn" <?php selected( $settings['default_age_group'], 'newborn' ); ?>><?php echo esc_html__( 'Newborn', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
    6941391                                                                        </select>
    6951392                                                                </td>
    … …  
    7051402                                                                </td>
    7061403                                                        </tr>
     1404                                                </table>
     1405                                        </div>
     1406
     1407                                        <div class="card" style="padding: 20px; margin-bottom: 20px;">
     1408                                                <h3><?php echo esc_html__( 'Variant & Attribute Optimization', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
     1409                                                <p class="description">
     1410                                                        <?php echo esc_html__( 'Extract and optimize essential Google Shopping attributes from WooCommerce and SureCart. WooCommerce variable products are automatically exported as individual items linked by <g:item_group_id>.', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1411                                                </p>
     1412                                                <table class="form-table" role="presentation">
    7071413                                                        <tr>
    708                                                                 <th scope="row"><label for="wipf_auto_color"><?php echo esc_html__( 'Smart Color Extraction', 'world-inspire-product-feed-for-google-shopping' ); ?></label></th>
     1414                                                                <th scope="row"><?php echo esc_html__( 'Smart Attribute Detection', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    7091415                                                                <td>
    710                                                                         <label>
    711                                                                                 <input type="checkbox" id="wipf_auto_color" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> />
    712                                                                                 <?php echo esc_html__( 'Automatically infer and populate Google standard colors from title and description', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1416                                                                        <label style="display: block; margin-bottom: 8px;">
     1417                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> />
     1418                                                                                <strong><?php echo esc_html__( 'Color Extraction (<g:color>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Infer Google standard color names from attributes, titles, or descriptions (over 35 jewelry/apparel shades).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1419                                                                        </label>
     1420                                                                        <label style="display: block; margin-bottom: 8px;">
     1421                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_material]" value="yes" <?php checked( $settings['auto_detect_material'] ?? 'yes', 'yes' ); ?> />
     1422                                                                                <strong><?php echo esc_html__( 'Material Extraction (<g:material>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect materials (Gold, Sterling Silver, Leather, Cotton, Gemstones, Titanium, Stainless Steel).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1423                                                                        </label>
     1424                                                                        <label style="display: block; margin-bottom: 8px;">
     1425                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_size]" value="yes" <?php checked( $settings['auto_detect_size'] ?? 'yes', 'yes' ); ?> />
     1426                                                                                <strong><?php echo esc_html__( 'Size Extraction (<g:size>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Extract sizes from variation attributes or titles (e.g. Size 7, One Size, 8mm, S, M, L, XL).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1427                                                                        </label>
     1428                                                                        <label style="display: block; margin-bottom: 8px;">
     1429                                                                                <input type="checkbox" name="wipf_feed_settings[auto_detect_pattern]" value="yes" <?php checked( $settings['auto_detect_pattern'] ?? 'yes', 'yes' ); ?> />
     1430                                                                                <strong><?php echo esc_html__( 'Pattern Extraction (<g:pattern>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect patterns (Solid, Striped, Floral, Beaded, Polka Dot, Geometric).', 'world-inspire-product-feed-for-google-shopping' ); ?>
    7131431                                                                        </label>
    7141432                                                                </td>
    7151433                                                        </tr>
    7161434                                                </table>
    717                                         <?php elseif ( 'shipping' === $active_tab ) : ?>
     1435                                        </div>
     1436
     1437                                        <div class="card" style="padding: 20px; margin-bottom: 20px;">
     1438                                                <h3><?php echo esc_html__( 'Google Shopping Custom Labels (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
     1439                                                <p class="description">
     1440                                                        <?php echo esc_html__( 'Custom labels allow you to segment and filter products in Google Ads and Performance Max (PMax) campaigns (e.g. by price brackets, inventory status, product category, clearance tags, or margin tiers).', 'world-inspire-product-feed-for-google-shopping' ); ?>
     1441                                                </p>
     1442                                                <p>
     1443                                                        <label>
     1444                                                                <input type="checkbox" name="wipf_feed_settings[enable_custom_labels]" value="yes" <?php checked( $settings['enable_custom_labels'] ?? 'yes', 'yes' ); ?> />
     1445                                                                <strong><?php echo esc_html__( 'Enable Custom Labels in Feed', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
     1446                                                        </label>
     1447                                                </p>
     1448
     1449                                                <table class="widefat striped" style="margin-top: 15px;">
     1450                                                        <thead>
     1451                                                                <tr>
     1452                                                                        <th style="width: 16%;"><?php echo esc_html__( 'Custom Label Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1453                                                                        <th style="width: 22%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1454                                                                        <th style="width: 38%;"><?php echo esc_html__( 'Rule Value / Configuration', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1455                                                                        <th style="width: 24%;"><?php echo esc_html__( 'Fallback Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1456                                                                </tr>
     1457                                                        </thead>
     1458                                                        <tbody>
     1459                                                                <?php
     1460                                                                $label_recommendations = array(
     1461                                                                        0 => array( 'name' => 'Custom Label 0', 'rec' => 'Price Tiers (< 25, 25-50, 50-100, 100+)', 'ph_val' => '25,50,100 (Thresholds)', 'ph_fb' => 'Standard' ),
     1462                                                                        1 => array( 'name' => 'Custom Label 1', 'rec' => 'Inventory Status (In Stock / Out of Stock)', 'ph_val' => 'Optional override: in_stock:Available, out_of_stock:Unavailable', 'ph_fb' => 'Out of Stock' ),
     1463                                                                        2 => array( 'name' => 'Custom Label 2', 'rec' => 'Category / Collection Name', 'ph_val' => 'Leave empty to use primary category', 'ph_fb' => 'General' ),
     1464                                                                        3 => array( 'name' => 'Custom Label 3', 'rec' => 'Promotional / Tag Keywords Match', 'ph_val' => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller', 'ph_fb' => 'Regular' ),
     1465                                                                        4 => array( 'name' => 'Custom Label 4', 'rec' => 'Static Value / Custom Post Meta Key', 'ph_val' => 'Enter static string or post_meta key', 'ph_fb' => '' ),
     1466                                                                );
     1467
     1468                                                                for ( $slot = 0; $slot <= 4; $slot++ ) :
     1469                                                                        $slot_type = $settings[ "custom_label_{$slot}_type" ] ?? 'disabled';
     1470                                                                        $slot_val  = $settings[ "custom_label_{$slot}_value" ] ?? '';
     1471                                                                        $slot_fb   = $settings[ "custom_label_{$slot}_fallback" ] ?? '';
     1472                                                                        $meta_info = $label_recommendations[ $slot ];
     1473                                                                        ?>
     1474                                                                        <tr>
     1475                                                                                <td>
     1476                                                                                        <strong><?php echo esc_html( $meta_info['name'] ); ?></strong>
     1477                                                                                        <br><span class="description" style="font-size: 11px;"><?php echo esc_html( $meta_info['rec'] ); ?></span>
     1478                                                                                </td>
     1479                                                                                <td>
     1480                                                                                        <select name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_type]" style="width: 100%;">
     1481                                                                                                <option value="disabled" <?php selected( $slot_type, 'disabled' ); ?>><?php echo esc_html__( 'Disabled', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1482                                                                                                <option value="price_tier" <?php selected( $slot_type, 'price_tier' ); ?>><?php echo esc_html__( 'Price Range Tier', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1483                                                                                                <option value="inventory" <?php selected( $slot_type, 'inventory' ); ?>><?php echo esc_html__( 'Inventory Status', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1484                                                                                                <option value="category" <?php selected( $slot_type, 'category' ); ?>><?php echo esc_html__( 'Product Category', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1485                                                                                                <option value="tag_keywords" <?php selected( $slot_type, 'tag_keywords' ); ?>><?php echo esc_html__( 'Tag & Keyword Matcher', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1486                                                                                                <option value="static" <?php selected( $slot_type, 'static' ); ?>><?php echo esc_html__( 'Static Custom String', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1487                                                                                                <option value="custom_meta" <?php selected( $slot_type, 'custom_meta' ); ?>><?php echo esc_html__( 'Custom Post Meta Field', 'world-inspire-product-feed-for-google-shopping' ); ?></option>
     1488                                                                                        </select>
     1489                                                                                </td>
     1490                                                                                <td>
     1491                                                                                        <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_value]" class="large-text" value="<?php echo esc_attr( $slot_val ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_val'] ); ?>" />
     1492                                                                                </td>
     1493                                                                                <td>
     1494                                                                                        <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_fallback]" class="regular-text" value="<?php echo esc_attr( $slot_fb ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_fb'] ); ?>" />
     1495                                                                                </td>
     1496                                                                        </tr>
     1497                                                                <?php endfor; ?>
     1498                                                        </tbody>
     1499                                                </table>
     1500                                        </div>
     1501                                <?php elseif ( 'shipping' === $active_tab ) : ?>
     1502                                        <div class="card" style="padding: 20px;">
    7181503                                                <h3><?php echo esc_html__( 'Multi-Country Shipping Generator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    7191504                                                <p><?php echo esc_html__( 'Configures embedded shipping rules for international Google Shopping campaigns.', 'world-inspire-product-feed-for-google-shopping' ); ?></p>
    … …  
    7411526                                                        </tr>
    7421527                                                </table>
    743                                         <?php endif; ?>
    744                                 </div>
     1528                                        </div>
     1529                                <?php endif; ?>
    7451530                                <?php submit_button(); ?>
    7461531                        </form>
    7471532                <?php elseif ( 'diagnostic' === $active_tab ) : ?>
    748                         <div class="card" style="max-width: 900px; margin-top: 20px; padding: 20px;">
    749                                 <h3><?php echo esc_html__( 'Live Feed Diagnostic Audit', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    750                                 <?php
    751                                 $sample_products = wipf_collect_products( 5 );
    752                                 $surecart_ok     = post_type_exists( 'sc_product' );
    753                                 $woocommerce_ok  = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) );
    754                                 ?>
     1533                        <?php
     1534                        $surecart_ok     = post_type_exists( 'sc_product' );
     1535                        $woocommerce_ok  = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) );
     1536                        $sample_products = wipf_collect_products( 5 );
     1537
     1538                        $inspect_id = isset( $_GET['inspect_id'] ) ? absint( $_GET['inspect_id'] ) : ( ! empty( $sample_products[0]['id'] ) ? (int) $sample_products[0]['id'] : 0 );
     1539                        $audit_data = $inspect_id > 0 ? wipf_diagnose_product( $inspect_id, $settings ) : null;
     1540                        ?>
     1541                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
     1542                                <h3><?php echo esc_html__( 'Live Feed Engine Status', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    7551543                                <p>
    7561544                                        <strong><?php echo esc_html__( 'SureCart Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
    757                                         <?php echo $surecart_ok ? '<span style="color:green;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
     1545                                        <?php echo $surecart_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
    7581546                                        &nbsp;|&nbsp;
    7591547                                        <strong><?php echo esc_html__( 'WooCommerce Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
    760                                         <?php echo $woocommerce_ok ? '<span style="color:green;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
     1548                                        <?php echo $woocommerce_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>
     1549                                        &nbsp;|&nbsp;
     1550                                        <strong><?php echo esc_html__( 'Catalog Sample Available:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
     1551                                        <code><?php echo esc_html( count( $sample_products ) ); ?></code>
    7611552                                </p>
    762                                 <p>
    763                                         <strong><?php echo esc_html__( 'Products Found in Catalog:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong>
    764                                         <?php echo esc_html( count( $sample_products ) ); ?> (<?php echo esc_html__( 'showing first 5', 'world-inspire-product-feed-for-google-shopping' ); ?>)
     1553                        </div>
     1554
     1555                        <!-- Product Inspector & XML Simulator -->
     1556                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
     1557                                <h3><?php echo esc_html__( 'Product Inspector & Diagnostic Simulator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
     1558                                <p class="description">
     1559                                        <?php echo esc_html__( 'Enter any Product ID to audit its exact extracted attributes, custom labels evaluation logic, and preview the generated Google Shopping <item> XML element.', 'world-inspire-product-feed-for-google-shopping' ); ?>
    7651560                                </p>
    7661561
     1562                                <form method="get" action="" style="margin: 15px 0 20px 0;">
     1563                                        <input type="hidden" name="page" value="wipf-settings" />
     1564                                        <input type="hidden" name="tab" value="diagnostic" />
     1565                                        <label for="inspect_id"><strong><?php echo esc_html__( 'Test Product ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong></label>
     1566                                        <input type="number" id="inspect_id" name="inspect_id" value="<?php echo esc_attr( $inspect_id ? (string) $inspect_id : '' ); ?>" placeholder="e.g. 790" style="width: 140px; margin: 0 10px;" />
     1567                                        <button type="submit" class="button button-primary"><?php echo esc_html__( 'Inspect Product', 'world-inspire-product-feed-for-google-shopping' ); ?></button>
     1568                                </form>
     1569
     1570                                <?php if ( $audit_data && ! empty( $audit_data['found'] ) ) : ?>
     1571                                        <?php
     1572                                        $p = $audit_data['product'];
     1573                                        ?>
     1574                                        <div style="background: #f0f6fc; border-left: 4px solid #72aee6; padding: 12px 16px; margin-bottom: 20px;">
     1575                                                <h4 style="margin: 0 0 8px 0;"><?php echo esc_html( $p['title'] ); ?> <span style="font-weight: normal; font-size: 13px;">(ID: <code><?php echo esc_html( $p['id'] ); ?></code> &middot; <?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?> &middot; <?php echo esc_html( $p['availability'] ); ?> &middot; Source: <code><?php echo esc_html( $p['source'] ); ?></code>)</span></h4>
     1576                                                <p style="margin: 0; font-size: 13px;">
     1577                                                        <strong><?php echo esc_html__( 'Variant Group ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <?php echo ! empty( $p['item_group_id'] ) ? '<code>' . esc_html( $p['item_group_id'] ) . '</code>' : '<em>' . esc_html__( 'None (Simple Product)', 'world-inspire-product-feed-for-google-shopping' ) . '</em>'; ?>
     1578                                                        &nbsp;|&nbsp;
     1579                                                        <strong><?php echo esc_html__( 'Color:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['color'] ?: '—' ); ?></code>
     1580                                                        &nbsp;|&nbsp;
     1581                                                        <strong><?php echo esc_html__( 'Size:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['size'] ?: '—' ); ?></code>
     1582                                                        &nbsp;|&nbsp;
     1583                                                        <strong><?php echo esc_html__( 'Material:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['material'] ?: '—' ); ?></code>
     1584                                                        &nbsp;|&nbsp;
     1585                                                        <strong><?php echo esc_html__( 'Pattern:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['pattern'] ?: '—' ); ?></code>
     1586                                                </p>
     1587                                        </div>
     1588
     1589                                        <h4><?php echo esc_html__( 'Custom Labels Evaluation Audit (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h4>
     1590                                        <table class="widefat striped" style="margin-bottom: 20px;">
     1591                                                <thead>
     1592                                                        <tr>
     1593                                                                <th style="width: 16%;"><?php echo esc_html__( 'Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1594                                                                <th style="width: 18%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1595                                                                <th style="width: 26%;"><?php echo esc_html__( 'Evaluated Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1596                                                                <th style="width: 40%;"><?php echo esc_html__( 'Matching Reason / Trace', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1597                                                        </tr>
     1598                                                </thead>
     1599                                                <tbody>
     1600                                                        <?php foreach ( $audit_data['labels'] as $slot_key => $eval ) : ?>
     1601                                                                <tr>
     1602                                                                        <td><code><?php echo esc_html( $slot_key ); ?></code></td>
     1603                                                                        <td><?php echo esc_html( ucfirst( str_replace( '_', ' ', $eval['type'] ) ) ); ?></td>
     1604                                                                        <td>
     1605                                                                                <?php if ( ! empty( $eval['value'] ) ) : ?>
     1606                                                                                        <span style="font-weight: 600; color: #135e96; background: #e7f3fa; padding: 2px 8px; border-radius: 3px;">
     1607                                                                                                <?php echo esc_html( $eval['value'] ); ?>
     1608                                                                                        </span>
     1609                                                                                <?php else : ?>
     1610                                                                                        <span style="color: #888;"><em><?php echo esc_html__( 'Empty (Omitted from XML)', 'world-inspire-product-feed-for-google-shopping' ); ?></em></span>
     1611                                                                                <?php endif; ?>
     1612                                                                        </td>
     1613                                                                        <td><small><?php echo esc_html( $eval['reason'] ); ?></small></td>
     1614                                                                </tr>
     1615                                                        <?php endforeach; ?>
     1616                                                </tbody>
     1617                                        </table>
     1618
     1619                                        <h4><?php echo esc_html__( 'Generated XML <item> Snippet Preview', 'world-inspire-product-feed-for-google-shopping' ); ?></h4>
     1620                                        <pre style="background: #2b2b2b; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; line-height: 1.5;"><code><?php echo esc_html( $audit_data['xml_snippet'] ); ?></code></pre>
     1621
     1622                                <?php elseif ( $audit_data && empty( $audit_data['found'] ) ) : ?>
     1623                                        <div class="notice notice-warning inline"><p><?php echo esc_html( $audit_data['error'] ); ?></p></div>
     1624                                <?php endif; ?>
     1625                        </div>
     1626
     1627                        <!-- Catalog Overview Sample Table -->
     1628                        <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;">
     1629                                <h3><?php echo esc_html__( 'Catalog Sample Inspection (First 5 Items)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3>
    7671630                                <?php if ( ! empty( $sample_products ) ) : ?>
    7681631                                        <table class="widefat striped" style="margin-top: 15px;">
    … …  
    7721635                                                                <th><?php echo esc_html__( 'Title', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    7731636                                                                <th><?php echo esc_html__( 'Price', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    774                                                                 <th><?php echo esc_html__( 'Availability', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    775                                                                 <th><?php echo esc_html__( 'Detected Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    776                                                                 <th><?php echo esc_html__( 'Engine', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1637                                                                <th><?php echo esc_html__( 'Group ID', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1638                                                                <th><?php echo esc_html__( 'Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1639                                                                <th><?php echo esc_html__( 'Size', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1640                                                                <th><?php echo esc_html__( 'Material', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1641                                                                <th><?php echo esc_html__( 'Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
     1642                                                                <th><?php echo esc_html__( 'Action', 'world-inspire-product-feed-for-google-shopping' ); ?></th>
    7771643                                                        </tr>
    7781644                                                </thead>
    7791645                                                <tbody>
    7801646                                                        <?php foreach ( $sample_products as $p ) : ?>
     1647                                                                <?php
     1648                                                                $p_eval0 = wipf_evaluate_custom_label( 0, $p, $settings );
     1649                                                                $p_eval1 = wipf_evaluate_custom_label( 1, $p, $settings );
     1650                                                                $label_summary = array();
     1651                                                                if ( ! empty( $p_eval0['value'] ) ) {
     1652                                                                        $label_summary[] = 'L0: ' . $p_eval0['value'];
     1653                                                                }
     1654                                                                if ( ! empty( $p_eval1['value'] ) ) {
     1655                                                                        $label_summary[] = 'L1: ' . $p_eval1['value'];
     1656                                                                }
     1657                                                                ?>
    7811658                                                                <tr>
    7821659                                                                        <td><code><?php echo esc_html( $p['id'] ); ?></code></td>
    7831660                                                                        <td><strong><?php echo esc_html( $p['title'] ); ?></strong></td>
    7841661                                                                        <td><?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?></td>
    785                                                                         <td><?php echo esc_html( $p['availability'] ); ?></td>
    786                                                                         <td><?php echo esc_html( wipf_detect_color( $p['title'], $p['description'] ) ?: '—' ); ?></td>
    787                                                                         <td><code><?php echo esc_html( $p['source'] ); ?></code></td>
     1662                                                                        <td><code><?php echo esc_html( $p['item_group_id'] ?: '—' ); ?></code></td>
     1663                                                                        <td><?php echo esc_html( $p['color'] ?: '—' ); ?></td>
     1664                                                                        <td><?php echo esc_html( $p['size'] ?: '—' ); ?></td>
     1665                                                                        <td><?php echo esc_html( $p['material'] ?: '—' ); ?></td>
     1666                                                                        <td><small><?php echo esc_html( ! empty( $label_summary ) ? implode( ' | ', $label_summary ) : '—' ); ?></small></td>
     1667                                                                        <td><a href="?page=wipf-settings&tab=diagnostic&inspect_id=<?php echo esc_attr( $p['id'] ); ?>" class="button button-small"><?php echo esc_html__( 'Inspect', 'world-inspire-product-feed-for-google-shopping' ); ?></a></td>
    7881668                                                                </tr>
    7891669                                                        <?php endforeach; ?>
Note: See TracChangeset for help on using the changeset viewer.