Plugin Directory

Changeset 3447731


Ignore:
Timestamp:
01/27/2026 10:14:19 AM (3 weeks ago)
Author:
2wstechnologies
Message:

Fix maxDiscount logic (in percentage case)
Add details about coupon (product or category)
Fix checkout failed when using ai generated coupon
Fix product update(error appears in shop previously)

Location:
convertybot
Files:
97 added
7 edited

Legend:

Unmodified
Added
Removed
  • convertybot/trunk/convertybot.php

    r3445734 r3447731  
    33 * Plugin Name: ConvertyBot
    44 * Description: An intelligent AI-powered sales assistant for WooCommerce that helps visitors discover products, provides personalized recommendations, and generates dynamic discount codes to boost sales.
    5  * Version: 1.0.29
     5 * Version: 1.0.31
    66 * Author: ConvertyBot, 2wstechnologies Team
    77 * Author URI: https://convertybot.com
     
    4242
    4343// Define plugin constants
    44 define('CONVERTYBOT_VERSION', '1.0.29');
     44define('CONVERTYBOT_VERSION', '1.0.31');
    4545define('CONVERTYBOT_PLUGIN_URL', plugin_dir_url(__FILE__));
    4646define('CONVERTYBOT_PLUGIN_PATH', plugin_dir_path(__FILE__));
  • convertybot/trunk/includes/class-coupon-manager.php

    r3437100 r3447731  
    3333        add_action('woocommerce_applied_coupon', array($this, 'track_coupon_application'));
    3434
    35         // ✅ FIX: Track actual coupon USAGE when order is placed/completed
     35        // Track actual coupon USAGE when order is placed/completed
    3636        // Multiple hooks to catch all scenarios:
    3737        // 1. woocommerce_checkout_order_processed - fires immediately when checkout is processed
     
    434434                $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons';
    435435
     436                // Get expiry date - try multiple sources
     437                $expiry_str = null;
     438
     439                // First try from WC coupon
    436440                $expiry_datetime = $coupon->get_date_expires();
    437                 $expiry_str = $expiry_datetime ? $expiry_datetime->date('Y-m-d H:i:s') : null;
     441                if ($expiry_datetime) {
     442                    $expiry_str = $expiry_datetime->date('Y-m-d H:i:s');
     443                }
     444
     445                // Fallback to original coupon_data
     446                if (empty($expiry_str)) {
     447                    if (!empty($coupon_data['expiresAt'])) {
     448                        try {
     449                            $dt = new DateTime($coupon_data['expiresAt']);
     450                            $expiry_str = $dt->format('Y-m-d H:i:s');
     451                        } catch (Exception $e) {
     452                            // Ignore parsing errors
     453                        }
     454                    } elseif (!empty($coupon_data['expiry_date'])) {
     455                        $expiry_str = $coupon_data['expiry_date'];
     456                    }
     457                }
     458
     459                // Final fallback: 24 hours from now
     460                if (empty($expiry_str)) {
     461                    $expiry_str = date('Y-m-d H:i:s', strtotime('+24 hours'));
     462                }
    438463
    439464                $wpdb->insert(
     
    469494    public function validate_ai_coupon($is_valid, $coupon) {
    470495        global $wpdb;
    471        
     496
    472497        $coupon_code = $coupon->get_code();
    473        
    474         // Check if it's an AI generated coupon
    475         if (strpos($coupon_code, 'AI') !== 0) {
     498
     499        // WooCommerce lowercases coupon codes, so convert to uppercase for comparison
     500        $coupon_code_upper = strtoupper($coupon_code);
     501
     502        // Check if it's an AI generated coupon (AI_ prefix)
     503        if (strpos($coupon_code_upper, 'AI_') !== 0) {
    476504            return $is_valid;
    477505        }
    478        
     506
    479507        $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons';
    480        
     508
     509        // Query with uppercase code (as stored in DB)
    481510        $ai_coupon = $wpdb->get_row(
    482511            $wpdb->prepare(
    483512                "SELECT * FROM $coupons_table WHERE coupon_code = %s",
    484                 $coupon_code
     513                $coupon_code_upper
    485514            )
    486515        );
    487        
     516
    488517        if (!$ai_coupon) {
     518            // Coupon not found in our tracking table - let WooCommerce handle validation
     519            return $is_valid;
     520        }
     521
     522        // Check if expired (ignore invalid dates like 1970-01-01 which indicate parsing failure)
     523        $expiry_timestamp = strtotime($ai_coupon->expiry_date);
     524        // Only check expiry if it's a valid date (after year 2000)
     525        if (!empty($ai_coupon->expiry_date) && $expiry_timestamp > 946684800 && $expiry_timestamp < time()) {
    489526            return false;
    490527        }
    491        
    492         // Check if expired
    493         if (strtotime($ai_coupon->expiry_date) < time()) {
    494             return false;
    495         }
    496        
     528
    497529        // Check if already used (if usage limit is 1)
    498530        if ($ai_coupon->usage_limit == 1 && $ai_coupon->is_used) {
    499531            return false;
    500532        }
    501        
     533
    502534        // Check usage count
    503535        if ($ai_coupon->usage_count >= $ai_coupon->usage_limit) {
    504536            return false;
    505537        }
    506        
     538
    507539        return $is_valid;
    508540    }
    509    
     541
    510542    /**
    511543     * Track coupon application (when added to cart)
     
    515547        global $wpdb;
    516548
    517         // ✅ FIX: Only track coupons starting with 'AI_' (our AI-generated coupons)
    518         if (strpos($coupon_code, 'AI_') !== 0) {
     549        // Only track coupons starting with 'AI_' (our AI-generated coupons)
     550        $coupon_upper = strtoupper($coupon_code);
     551        if (strpos($coupon_upper, 'AI_') !== 0) {
    519552            return; // Not our coupon, skip
    520553        }
     
    528561                 SET status = 'applied'
    529562                 WHERE coupon_code = %s AND status = 'active'",
    530                 $coupon_code
     563                strtoupper($coupon_code)
    531564            )
    532565        );
     
    538571     */
    539572    public function track_coupon_order_usage($order_id, $posted_data, $order) {
    540         global $wpdb;
    541 
    542         if (!$order_id) {
    543             return;
    544         }
    545 
    546         // Get the order
    547         if (!is_object($order)) {
    548             $order = wc_get_order($order_id);
    549         }
    550 
    551         if (!$order) {
    552             return;
    553         }
    554 
    555         // Get coupons used in this order
    556         $used_coupons = $order->get_coupon_codes();
    557 
    558         if (empty($used_coupons)) {
    559             return;
    560         }
    561 
    562         $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons';
    563 
    564         foreach ($used_coupons as $coupon_code) {
    565             // ✅ FIX: Only track coupons starting with 'ai_' or 'ai' (our AI-generated coupons - lowercase due to WooCommerce)
    566             // WooCommerce automatically converts coupon codes to lowercase
    567             $coupon_lower = strtolower($coupon_code);
    568             if (strpos($coupon_lower, 'ai_') !== 0 && strpos($coupon_lower, 'ai') !== 0) {
    569                 continue; // Not our coupon, skip
    570             }
    571 
    572             // ✅ FIX: Normalize to lowercase for DB lookup (WooCommerce stores in lowercase)
    573             $coupon_code_normalized = $coupon_lower;
    574 
    575             // Get the actual discount amount for this coupon from the order
    576             $discount_amount = 0;
    577             foreach ($order->get_items('coupon') as $item_id => $item) {
    578                 if ($item->get_code() === $coupon_code) {
    579                     $discount_amount = abs(floatval($item->get_discount()));
    580                     break;
     573        // CRITICAL: Wrap everything in try-catch to NEVER block checkout
     574        try {
     575            global $wpdb;
     576
     577            if (!$order_id) {
     578                return;
     579            }
     580
     581            // Get the order
     582            if (!is_object($order)) {
     583                $order = wc_get_order($order_id);
     584            }
     585
     586            if (!$order) {
     587                return;
     588            }
     589
     590            // Get coupons used in this order
     591            $used_coupons = $order->get_coupon_codes();
     592
     593            if (empty($used_coupons)) {
     594                return;
     595            }
     596
     597            $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons';
     598
     599            foreach ($used_coupons as $coupon_code) {
     600                // Only track coupons starting with 'AI_' (our AI-generated coupons)
     601                // WooCommerce automatically converts coupon codes to lowercase
     602                $coupon_upper = strtoupper($coupon_code);
     603                if (strpos($coupon_upper, 'AI_') !== 0) {
     604                    continue; // Not our coupon, skip
    581605                }
    582             }
    583 
    584             // Update tracking table with REAL usage data
    585             // Use normalized lowercase code for DB lookup
    586             $wpdb->query(
    587                 $wpdb->prepare(
    588                     "UPDATE $coupons_table
    589                      SET usage_count = usage_count + 1,
    590                          is_used = 1,
    591                          used_at = NOW(),
    592                          order_id = %d,
    593                          actual_discount_amount = %f,
    594                          status = CASE
    595                              WHEN usage_count + 1 >= usage_limit THEN 'used'
    596                              ELSE 'active'
    597                          END
    598                      WHERE coupon_code = %s",
    599                     $order_id,
    600                     $discount_amount,
    601                     $coupon_code_normalized
    602                 )
    603             );
    604 
    605             // Notify backend API that coupon was used
    606             // This updates MongoDB so AI won't re-offer the same coupon
    607             // Send UPPERCASE code since that's how it's stored in MongoDB (AI_XXXX format)
    608             $coupon_code_upper = strtoupper($coupon_code);
    609             $this->notify_backend_coupon_used($coupon_code_upper, $order_id, $discount_amount);
     606
     607                // Our coupons are stored as uppercase (AI_XXXXXXXX) in the DB
     608                $coupon_code_normalized = $coupon_upper;
     609
     610                // Get the actual discount amount for this coupon from the order
     611                $discount_amount = 0;
     612                foreach ($order->get_items('coupon') as $item_id => $item) {
     613                    if ($item->get_code() === $coupon_code) {
     614                        $discount_amount = abs(floatval($item->get_discount()));
     615                        break;
     616                    }
     617                }
     618
     619                // Update tracking table with REAL usage data
     620                $wpdb->query(
     621                    $wpdb->prepare(
     622                        "UPDATE $coupons_table
     623                         SET usage_count = usage_count + 1,
     624                             is_used = 1,
     625                             used_at = NOW(),
     626                             order_id = %d,
     627                             actual_discount_amount = %f,
     628                             status = CASE
     629                                 WHEN usage_count + 1 >= usage_limit THEN 'used'
     630                                 ELSE 'active'
     631                             END
     632                         WHERE coupon_code = %s",
     633                        $order_id,
     634                        $discount_amount,
     635                        $coupon_code_normalized
     636                    )
     637                );
     638
     639                // Queue backend notification for later (don't block checkout)
     640                // The actual API call will happen via shutdown hook
     641                $this->queue_backend_notification($coupon_upper, $order_id, $discount_amount);
     642            }
     643        } catch (Exception $e) {
     644            error_log('ConvertyBot coupon tracking EXCEPTION: ' . $e->getMessage());
     645        } catch (Error $e) {
     646            error_log('ConvertyBot coupon tracking ERROR: ' . $e->getMessage());
    610647        }
    611648
     
    648685        global $wpdb;
    649686
    650         if (!$order_id) {
    651             return;
    652         }
    653 
    654         $order = wc_get_order($order_id);
    655 
    656         if (!$order) {
    657             return;
    658         }
    659 
    660         // Get coupons used in this order
    661         $used_coupons = $order->get_coupon_codes();
    662 
    663         if (empty($used_coupons)) {
    664             return;
    665         }
    666 
    667         $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons';
    668 
    669         foreach ($used_coupons as $coupon_code) {
    670             // ✅ FIX: Only track coupons starting with 'AI_' or 'ai_' (WooCommerce lowercases codes)
    671             $coupon_code_lower = strtolower($coupon_code);
    672             if (strpos($coupon_code_lower, 'ai_') !== 0) {
    673                 continue; // Not our coupon, skip
    674             }
    675 
    676             // Check if already marked as used
    677             $already_tracked = $wpdb->get_var(
    678                 $wpdb->prepare(
    679                     "SELECT is_used FROM $coupons_table WHERE coupon_code = %s",
    680                     $coupon_code
    681                 )
    682             );
    683 
    684             // If already marked as used, skip
    685             if ($already_tracked) {
    686                 continue;
    687             }
    688 
    689             // Get the actual discount amount
    690             $discount_amount = 0;
    691             foreach ($order->get_items('coupon') as $item_id => $item) {
    692                 if ($item->get_code() === $coupon_code) {
    693                     $discount_amount = abs(floatval($item->get_discount()));
    694                     break;
     687        try {
     688            if (!$order_id) {
     689                return;
     690            }
     691
     692            $order = wc_get_order($order_id);
     693
     694            if (!$order) {
     695                return;
     696            }
     697
     698            // Get coupons used in this order
     699            $used_coupons = $order->get_coupon_codes();
     700
     701            if (empty($used_coupons)) {
     702                return;
     703            }
     704
     705            $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons';
     706
     707            foreach ($used_coupons as $coupon_code) {
     708                // Only track coupons starting with 'AI_' (WooCommerce lowercases codes)
     709                $coupon_upper = strtoupper($coupon_code);
     710                if (strpos($coupon_upper, 'AI_') !== 0) {
     711                    continue; // Not our coupon, skip
    695712                }
    696             }
    697 
    698             // Mark as used with actual discount
    699             $wpdb->query(
    700                 $wpdb->prepare(
    701                     "UPDATE $coupons_table
    702                      SET is_used = 1,
    703                          used_at = NOW(),
    704                          order_id = %d,
    705                          actual_discount_amount = %f,
    706                          status = 'used'
    707                      WHERE coupon_code = %s",
    708                     $order_id,
    709                     $discount_amount,
    710                     $coupon_code
    711                 )
    712             );
    713 
    714             // Notify backend API that coupon was used
    715             // This updates MongoDB so AI won't re-offer the same coupon
    716             // Send UPPERCASE code since that's how it's stored in MongoDB (AI_XXXX format)
    717             $coupon_code_upper = strtoupper($coupon_code);
    718             $this->notify_backend_coupon_used($coupon_code_upper, $order_id, $discount_amount);
    719         }
    720     }
    721 
    722     /**
    723      * Notify backend API that a coupon was used
    724      * This is critical to prevent AI from re-offering used coupons
    725      */
    726     private function notify_backend_coupon_used($coupon_code, $order_id, $discount_amount) {
    727         // Use the plugin's get_api_url() method for consistency
    728         // This returns the full API URL with /api prefix (e.g., https://xxx:8443/api)
    729         $plugin = convertybot();
    730         $api_url = $plugin->get_api_url();
    731 
    732         // Get API key from options
    733         $options = $plugin->get_options();
    734         $api_key = isset($options['api_key']) ? $options['api_key'] : '';
    735 
    736         if (empty($api_url) || empty($api_key)) {
    737             return false;
    738         }
    739 
    740         // Remove trailing slash from API URL
    741         $api_url = rtrim($api_url, '/');
    742 
    743         // Build the webhook URL (api_url already includes /api, so append /webhooks/...)
    744         $webhook_url = $api_url . '/webhooks/woocommerce/coupon/used';
    745 
    746         $payload = array(
     713
     714                // Check if already marked as used
     715                $already_tracked = $wpdb->get_var(
     716                    $wpdb->prepare(
     717                        "SELECT is_used FROM $coupons_table WHERE coupon_code = %s",
     718                        $coupon_upper
     719                    )
     720                );
     721
     722                // If already marked as used, skip
     723                if ($already_tracked) {
     724                    continue;
     725                }
     726
     727                // Get the actual discount amount
     728                $discount_amount = 0;
     729                foreach ($order->get_items('coupon') as $item_id => $item) {
     730                    if (strtoupper($item->get_code()) === $coupon_upper) {
     731                        $discount_amount = abs(floatval($item->get_discount()));
     732                        break;
     733                    }
     734                }
     735
     736                // Mark as used with actual discount
     737                $wpdb->query(
     738                    $wpdb->prepare(
     739                        "UPDATE $coupons_table
     740                         SET is_used = 1,
     741                             used_at = NOW(),
     742                             order_id = %d,
     743                             actual_discount_amount = %f,
     744                             status = 'used'
     745                         WHERE coupon_code = %s",
     746                        $order_id,
     747                        $discount_amount,
     748                        $coupon_upper
     749                    )
     750                );
     751
     752                // Queue backend notification (don't block)
     753                $this->queue_backend_notification($coupon_upper, $order_id, $discount_amount);
     754            }
     755        } catch (Exception $e) {
     756            error_log('ConvertyBot coupon completion tracking error: ' . $e->getMessage());
     757        } catch (Error $e) {
     758            error_log('ConvertyBot coupon completion tracking error: ' . $e->getMessage());
     759        }
     760    }
     761
     762    /**
     763     * Queue backend notification for processing outside of checkout
     764     * This prevents checkout from hanging due to slow API calls
     765     */
     766    private function queue_backend_notification($coupon_code, $order_id, $discount_amount) {
     767        // Store in a transient queue
     768        $queue = get_option('convertybot_coupon_notify_queue', array());
     769        $queue[] = array(
    747770            'coupon_code' => $coupon_code,
    748771            'order_id' => $order_id,
    749772            'discount_amount' => $discount_amount,
    750             'used_at' => current_time('mysql'),
    751             'shop_url' => get_site_url()
    752         );
    753 
    754         $response = wp_remote_post($webhook_url, array(
    755             'headers' => array(
    756                 'Content-Type' => 'application/json',
    757                 'X-API-Key' => $api_key
    758             ),
    759             'body' => json_encode($payload),
    760             'timeout' => 15
    761         ));
    762 
    763         if (is_wp_error($response)) {
     773            'queued_at' => current_time('mysql')
     774        );
     775        update_option('convertybot_coupon_notify_queue', $queue);
     776
     777        // Schedule immediate processing via shutdown hook (after checkout completes)
     778        if (!has_action('shutdown', array($this, 'process_notification_queue'))) {
     779            add_action('shutdown', array($this, 'process_notification_queue'));
     780        }
     781    }
     782
     783    /**
     784     * Process the notification queue (runs after checkout completes)
     785     */
     786    public function process_notification_queue() {
     787        $queue = get_option('convertybot_coupon_notify_queue', array());
     788
     789        if (empty($queue)) {
     790            return;
     791        }
     792
     793        foreach ($queue as $notification) {
     794            $this->notify_backend_coupon_used(
     795                $notification['coupon_code'],
     796                $notification['order_id'],
     797                $notification['discount_amount']
     798            );
     799        }
     800
     801        // Clear the queue
     802        update_option('convertybot_coupon_notify_queue', array());
     803    }
     804
     805    /**
     806     * Notify backend API that a coupon was used
     807     * This is critical to prevent AI from re-offering used coupons
     808     */
     809    private function notify_backend_coupon_used($coupon_code, $order_id, $discount_amount) {
     810        try {
     811            // Get options DIRECTLY from WordPress (safer during checkout)
     812            $options = get_option('convertybot_options', array());
     813
     814            // Get API URL
     815            $api_url = isset($options['api_url']) ? $options['api_url'] : 'https://api.convertybot.com/api';
     816
     817            // Get and decrypt API key
     818            $api_key_encrypted = isset($options['api_key']) ? $options['api_key'] : '';
     819            $api_key = '';
     820
     821            if (!empty($api_key_encrypted)) {
     822                // Try to decrypt using plugin if available
     823                if (function_exists('convertybot') && method_exists(convertybot(), 'decrypt_api_key')) {
     824                    $api_key = convertybot()->decrypt_api_key($api_key_encrypted);
     825                } else {
     826                    // Fallback: use directly (might be unencrypted)
     827                    $api_key = $api_key_encrypted;
     828                }
     829            }
     830
     831            if (empty($api_url) || empty($api_key)) {
     832                return false;
     833            }
     834
     835            // Remove trailing slash from API URL
     836            $api_url = rtrim($api_url, '/');
     837
     838            // Build the webhook URL
     839            $webhook_url = $api_url . '/webhooks/woocommerce/coupon/used';
     840
     841            $payload = array(
     842                'coupon_code' => $coupon_code,
     843                'order_id' => $order_id,
     844                'discount_amount' => $discount_amount,
     845                'used_at' => current_time('mysql'),
     846                'shop_url' => get_site_url()
     847            );
     848
     849            // Use SHORT timeout - if backend is slow, don't block forever
     850            // 2 second timeout is enough for a healthy API
     851            $response = wp_remote_post($webhook_url, array(
     852                'headers' => array(
     853                    'Content-Type' => 'application/json',
     854                    'X-API-Key' => $api_key
     855                ),
     856                'body' => json_encode($payload),
     857                'timeout' => 2,
     858                'sslverify' => false
     859            ));
     860
     861            if (is_wp_error($response)) {
     862                return false;
     863            }
     864
     865            $status_code = wp_remote_retrieve_response_code($response);
     866
     867            return ($status_code === 200);
     868        } catch (Exception $e) {
    764869            return false;
    765         }
    766 
    767         $status_code = wp_remote_retrieve_response_code($response);
    768 
    769         if ($status_code === 200) {
    770             return true;
    771         } else {
     870        } catch (Error $e) {
    772871            return false;
    773872        }
  • convertybot/trunk/includes/class-product-sync.php

    r3437100 r3447731  
    545545     */
    546546    private function get_related_products($product_id, $limit = 10) {
    547         $related = wc_get_product_related_posts($product_id, $limit);
     547        // Use the correct WooCommerce function
     548        $related = wc_get_related_products($product_id, $limit);
    548549        return array_map('intval', $related);
    549550    }
  • convertybot/trunk/includes/class-user-profile.php

    r3437100 r3447731  
    361361   
    362362    /**
     363     * Track purchase intent on thank you page
     364     * Hooked to woocommerce_thankyou
     365     */
     366    public function track_purchase_intent($order_id) {
     367        // This is just for tracking intent - actual purchase tracking happens in track_purchase
     368        // No action needed here, kept for hook compatibility
     369    }
     370
     371    /**
    363372     * Track WooCommerce purchase
    364373     */
    365374    public function track_purchase($order_id) {
    366         global $wpdb;
    367        
    368         $order = wc_get_order($order_id);
    369         if (!$order) return;
    370        
    371         // Get user identifier
    372         $user_id = $order->get_user_id();
    373         $user_email = $order->get_billing_email();
    374         $user_identifier = $user_id ? 'user_' . $user_id : 'email_' . md5($user_email);
    375        
    376         // Get or create profile
    377         $profile = $this->get_or_create_profile($user_identifier);
    378        
    379         // Get purchase history
    380         $purchase_history = $profile['purchase_history'] ?: array();
    381        
    382         // Add order items to purchase history
    383         foreach ($order->get_items() as $item) {
    384             $product = $item->get_product();
    385             if (!$product) continue;
    386            
    387             $purchase_history[] = array(
    388                 'productId' => $product->get_id(),
    389                 'productName' => $product->get_name(),
    390                 'category' => $this->get_product_categories($product->get_id()),
    391                 'price' => $item->get_total(),
    392                 'purchaseDate' => $order->get_date_created()->format('Y-m-d H:i:s'),
    393                 'orderId' => $order_id
     375        try {
     376            global $wpdb;
     377
     378            $order = wc_get_order($order_id);
     379            if (!$order) {
     380                return;
     381            }
     382
     383            // Get user identifier
     384            $user_id = $order->get_user_id();
     385            $user_email = $order->get_billing_email();
     386            $user_identifier = $user_id ? 'user_' . $user_id : 'email_' . md5($user_email);
     387
     388            // Get or create profile
     389            $profile = $this->get_or_create_profile($user_identifier);
     390
     391            // Get purchase history
     392            $purchase_history = $profile['purchase_history'] ?: array();
     393
     394            // Add order items to purchase history
     395            foreach ($order->get_items() as $item) {
     396                $product = $item->get_product();
     397                if (!$product) continue;
     398
     399                $purchase_history[] = array(
     400                    'productId' => $product->get_id(),
     401                    'productName' => $product->get_name(),
     402                    'category' => $this->get_product_categories($product->get_id()),
     403                    'price' => $item->get_total(),
     404                    'purchaseDate' => $order->get_date_created()->format('Y-m-d H:i:s'),
     405                    'orderId' => $order_id
     406                );
     407            }
     408
     409            // Update profile
     410            $table = $wpdb->prefix . 'convertybot_user_profiles';
     411            $wpdb->update(
     412                $table,
     413                array(
     414                    'purchase_history' => json_encode($purchase_history),
     415                    'total_purchases' => $wpdb->get_var($wpdb->prepare(
     416                        "SELECT total_purchases FROM $table WHERE user_identifier = %s",
     417                        $user_identifier
     418                    )) + 1,
     419                    'total_spent' => $wpdb->get_var($wpdb->prepare(
     420                        "SELECT total_spent FROM $table WHERE user_identifier = %s",
     421                        $user_identifier
     422                    )) + $order->get_total()
     423                ),
     424                array('user_identifier' => $user_identifier)
    394425            );
    395         }
    396        
    397         // Update profile
    398         $table = $wpdb->prefix . 'convertybot_user_profiles';
    399         $wpdb->update(
    400             $table,
    401             array(
    402                 'purchase_history' => json_encode($purchase_history),
    403                 'total_purchases' => $wpdb->get_var($wpdb->prepare(
    404                     "SELECT total_purchases FROM $table WHERE user_identifier = %s",
    405                     $user_identifier
    406                 )) + 1,
    407                 'total_spent' => $wpdb->get_var($wpdb->prepare(
    408                     "SELECT total_spent FROM $table WHERE user_identifier = %s",
    409                     $user_identifier
    410                 )) + $order->get_total()
    411             ),
    412             array('user_identifier' => $user_identifier)
    413         );
    414        
    415         // Send to backend API
    416         $this->sync_profile_to_backend($user_identifier);
     426
     427            // Send to backend API
     428            $this->sync_profile_to_backend($user_identifier);
     429
     430        } catch (Exception $e) {
     431            error_log('ConvertyBot user profile tracking error: ' . $e->getMessage());
     432        } catch (Error $e) {
     433            error_log('ConvertyBot user profile tracking error: ' . $e->getMessage());
     434        }
    417435    }
    418436   
  • convertybot/trunk/includes/class-user-tracking-enhanced.php

    r3444237 r3447731  
    476476   
    477477    public function track_purchase($order_id) {
    478         $tracking_enabled = $this->is_tracking_enabled();
    479         $privacy_ok = $this->check_privacy_compliance();
    480 
    481         if (!$tracking_enabled || !$privacy_ok) {
    482             return;
    483         }
    484 
    485         // IMPORTANT: Validate order_id and get order object
    486         if (empty($order_id) || !is_numeric($order_id)) {
    487             return;
    488         }
    489 
    490         $order = wc_get_order($order_id);
    491 
    492         if (!$order) {
    493             return;
    494         }
    495 
    496         // IMPORTANT: Prevent duplicate tracking if hook fires multiple times
    497         $tracking_key = 'convertybot_tracked_order_' . $order_id;
    498         if (get_transient($tracking_key)) {
    499             return;
    500         }
    501 
    502         // Get session ID (may be null)
    503         $session_id = $this->get_current_session_id();
    504 
    505         // Get user ID or customer ID
    506         $user_id = get_current_user_id();
    507         $customer_id = $order->get_customer_id();
    508 
    509         // Determine user identifier (guest ID, user ID, or customer ID)
    510         $user_identifier = null;
    511 
    512         // Try to get guest_id from localStorage via cookie (if frontend sets it)
    513         if (isset($_COOKIE['convertybot_guest_id']) && !empty($_COOKIE['convertybot_guest_id'])) {
    514             $user_identifier = sanitize_text_field($_COOKIE['convertybot_guest_id']);
    515         }
    516         // Otherwise use WordPress user ID
    517         elseif ($user_id && $user_id > 0) {
    518             $user_identifier = 'user_' . $user_id;
    519         }
    520         // Otherwise use WooCommerce customer ID (MUST be valid integer > 0)
    521         elseif ($customer_id && is_numeric($customer_id) && intval($customer_id) > 0) {
    522             $user_identifier = 'customer_' . intval($customer_id);
    523         }
    524         // Last resort: create unique guest ID from order
    525         else {
    526             // Create a persistent guest identifier from order data
    527             $billing_email = $order->get_billing_email();
    528             if ($billing_email) {
    529                 // Use email hash as guest identifier (persistent across orders)
    530                 $user_identifier = 'guest_email_' . substr(md5($billing_email), 0, 16);
    531             } else {
    532                 // Ultimate fallback: order-based identifier
    533                 $user_identifier = 'guest_order_' . $order_id;
    534             }
    535         }
    536 
    537         // Build product IDs array
    538         $product_ids = array();
    539         foreach ($order->get_items() as $item) {
    540             $product_ids[] = $item->get_product_id();
    541         }
    542 
    543         // Ensure all required fields have valid values (never NULL or empty)
    544         $conversion_data = array(
    545             'sessionId' => $session_id, // May be null, backend handles it
    546             'siteId' => $this->get_site_id(),
    547             'userId' => $user_identifier, // Always set (validated above)
    548             'orderId' => (string)$order_id, // Guaranteed valid from validation above
    549             'customerId' => ($customer_id && is_numeric($customer_id)) ? (int)$customer_id : 0,
    550             'conversionValue' => floatval($order->get_total()),
    551             'productIds' => $product_ids,
    552             'productCount' => count($product_ids),
    553             'couponCode' => implode(',', $order->get_coupon_codes()),
    554             'paymentMethod' => $order->get_payment_method() ? $order->get_payment_method() : 'unknown',
    555             'timestamp' => current_time('c')
    556         );
    557 
    558         $result = $this->send_to_backend('/tracking/conversion', $conversion_data);
    559 
    560         if ($result) {
    561             // Mark this order as tracked ONLY after successful backend response
     478        try {
     479            $tracking_enabled = $this->is_tracking_enabled();
     480            $privacy_ok = $this->check_privacy_compliance();
     481
     482            if (!$tracking_enabled || !$privacy_ok) {
     483                return;
     484            }
     485
     486            // IMPORTANT: Validate order_id and get order object
     487            if (empty($order_id) || !is_numeric($order_id)) {
     488                return;
     489            }
     490
     491            $order = wc_get_order($order_id);
     492
     493            if (!$order) {
     494                return;
     495            }
     496
     497            // IMPORTANT: Prevent duplicate tracking if hook fires multiple times
     498            $tracking_key = 'convertybot_tracked_order_' . $order_id;
     499            if (get_transient($tracking_key)) {
     500                return;
     501            }
     502
     503            // Get session ID (may be null)
     504            $session_id = $this->get_current_session_id();
     505
     506            // Get user ID or customer ID
     507            $user_id = get_current_user_id();
     508            $customer_id = $order->get_customer_id();
     509
     510            // Determine user identifier (guest ID, user ID, or customer ID)
     511            $user_identifier = null;
     512
     513            // Try to get guest_id from localStorage via cookie (if frontend sets it)
     514            if (isset($_COOKIE['convertybot_guest_id']) && !empty($_COOKIE['convertybot_guest_id'])) {
     515                $user_identifier = sanitize_text_field($_COOKIE['convertybot_guest_id']);
     516            }
     517            // Otherwise use WordPress user ID
     518            elseif ($user_id && $user_id > 0) {
     519                $user_identifier = 'user_' . $user_id;
     520            }
     521            // Otherwise use WooCommerce customer ID (MUST be valid integer > 0)
     522            elseif ($customer_id && is_numeric($customer_id) && intval($customer_id) > 0) {
     523                $user_identifier = 'customer_' . intval($customer_id);
     524            }
     525            // Last resort: create unique guest ID from order
     526            else {
     527                // Create a persistent guest identifier from order data
     528                $billing_email = $order->get_billing_email();
     529                if ($billing_email) {
     530                    // Use email hash as guest identifier (persistent across orders)
     531                    $user_identifier = 'guest_email_' . substr(md5($billing_email), 0, 16);
     532                } else {
     533                    // Ultimate fallback: order-based identifier
     534                    $user_identifier = 'guest_order_' . $order_id;
     535                }
     536            }
     537
     538            // Build product IDs array
     539            $product_ids = array();
     540            foreach ($order->get_items() as $item) {
     541                $product_ids[] = $item->get_product_id();
     542            }
     543
     544            // Ensure all required fields have valid values (never NULL or empty)
     545            $conversion_data = array(
     546                'sessionId' => $session_id, // May be null, backend handles it
     547                'siteId' => $this->get_site_id(),
     548                'userId' => $user_identifier, // Always set (validated above)
     549                'orderId' => (string)$order_id, // Guaranteed valid from validation above
     550                'customerId' => ($customer_id && is_numeric($customer_id)) ? (int)$customer_id : 0,
     551                'conversionValue' => floatval($order->get_total()),
     552                'productIds' => $product_ids,
     553                'productCount' => count($product_ids),
     554                'couponCode' => implode(',', $order->get_coupon_codes()),
     555                'paymentMethod' => $order->get_payment_method() ? $order->get_payment_method() : 'unknown',
     556                'timestamp' => current_time('c')
     557            );
     558
     559            // Use non-blocking mode to not delay thank you page loading
     560            $this->send_to_backend('/tracking/conversion', $conversion_data, false);
     561
     562            // Mark as tracked immediately (we fired the request, even if non-blocking)
    562563            set_transient($tracking_key, true, HOUR_IN_SECONDS);
     564
     565        } catch (Exception $e) {
     566            error_log('ConvertyBot enhanced tracking error: ' . $e->getMessage());
     567        } catch (Error $e) {
     568            error_log('ConvertyBot enhanced tracking error: ' . $e->getMessage());
    563569        }
    564570    }
     
    624630    }
    625631   
    626     private function send_to_backend($endpoint, $data) {
     632    private function send_to_backend($endpoint, $data, $blocking = true) {
    627633        $url = $this->backend_api_url . $endpoint;
    628634
     
    642648            ),
    643649            'timeout' => 10,
     650            'blocking' => $blocking, // Can be non-blocking for checkout performance
    644651            'sslverify' => false // Allow self-signed certificates
    645652        ));
     653
     654        if (!$blocking) {
     655            return true; // Non-blocking requests always return true
     656        }
    646657
    647658        if (is_wp_error($response)) {
  • convertybot/trunk/includes/class-user-tracking.php

    r3437100 r3447731  
    334334     */
    335335    public function track_purchase($order_id, $posted_data, $order) {
    336         $session_id = $this->get_current_session_id();
    337        
    338         if (!$session_id) {
    339             return;
    340         }
    341        
    342         // Mark products as purchased in engagement data
    343         foreach ($order->get_items() as $item) {
    344             $product_id = $item->get_product_id();
    345            
    346             $this->update_engagement_data($session_id, $product_id, array(
    347                 'purchase' => 1
     336        try {
     337            $session_id = $this->get_current_session_id();
     338
     339            if (!$session_id) {
     340                return;
     341            }
     342
     343            // Mark products as purchased in engagement data
     344            foreach ($order->get_items() as $item) {
     345                $product_id = $item->get_product_id();
     346
     347                $this->update_engagement_data($session_id, $product_id, array(
     348                    'purchase' => 1
     349                ));
     350            }
     351
     352            // Update session with conversion data
     353            global $wpdb;
     354
     355            $sessions_table = $wpdb->prefix . 'convertybot_chat_sessions';
     356
     357            $wpdb->update(
     358                $sessions_table,
     359                array(
     360                    'conversion_value' => $order->get_total(),
     361                    'status' => 'ended'
     362                ),
     363                array('session_id' => $session_id)
     364            );
     365
     366            // Track conversion event
     367            $this->track_ecommerce_event('purchase', array(
     368                'order_id' => $order_id,
     369                'total' => $order->get_total(),
     370                'currency' => $order->get_currency(),
     371                'items' => $this->get_order_items_data($order)
    348372            ));
    349         }
    350        
    351         // Update session with conversion data
    352         global $wpdb;
    353        
    354         $sessions_table = $wpdb->prefix . 'convertybot_chat_sessions';
    355        
    356         $wpdb->update(
    357             $sessions_table,
    358             array(
    359                 'conversion_value' => $order->get_total(),
    360                 'status' => 'ended'
    361             ),
    362             array('session_id' => $session_id)
    363         );
    364        
    365         // Track conversion event
    366         $this->track_ecommerce_event('purchase', array(
    367             'order_id' => $order_id,
    368             'total' => $order->get_total(),
    369             'currency' => $order->get_currency(),
    370             'items' => $this->get_order_items_data($order)
    371         ));
    372     }
    373    
     373        } catch (Exception $e) {
     374            error_log('ConvertyBot track_purchase error: ' . $e->getMessage());
     375        } catch (Error $e) {
     376            error_log('ConvertyBot track_purchase error: ' . $e->getMessage());
     377        }
     378    }
     379
    374380    /**
    375381     * Track ecommerce event
  • convertybot/trunk/readme.txt

    r3445734 r3447731  
    66Tested up to: 6.9
    77Requires PHP: 7.2
    8 Stable tag: 1.0.29
     8Stable tag: 1.0.31
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    249249== Changelog ==
    250250
     251= 1.0.31 =
     252* FIXED: Checkout "Service Unavailable" error when using AI-generated coupons
     253* FIXED: Backend API notification was using encrypted API key instead of decrypted key
     254* FIXED: Coupon usage not being properly tracked in MongoDB backend
     255* IMPROVED: Added 2-second timeout for backend API calls to prevent checkout hanging
     256* IMPROVED: Better error handling for coupon tracking during checkout
     257
     258= 1.0.30 =
     259* FIXED: Fatal error when updating products (wc_get_product_related_posts -> wc_get_related_products)
     260* FIXED: Product sync crash on update operation
     261
    251262= 1.0.29 =
    252263* FIXED: Chat button not reappearing after minimizing chat (inline styles from auto_open were persisting)
     
    374385== Upgrade Notice ==
    375386
     387= 1.0.31 =
     388Critical fix - Resolves "Service Unavailable" error during checkout when using AI-generated coupons. The backend API notification was using an encrypted API key which caused authentication failures. Coupon usage is now properly tracked in the backend.
     389
    376390= 1.0.29 =
    377 Critical fix - Chat button now properly reappears after minimizing the chat. Also includes p
     391Critical fix - Chat button now properly reappears after minimizing the chat. Also includes production-ready clean code with all debug console.logs removed.
     392
     393= 1.0.28 =
     394Fix for empty insight badges - The {{this}} template placeholder in visitor journey timelines was being replaced with empty string before the #each loop could process it. Insights now display correctly.
     395
     396= 1.0.27 =
     397Debug update - Added logging to diagnose empty insight badges in visitor journeys. Check browser console for insights data.
     398
     399= 1.0.26 =
     400Critical fix - Chat window can now be properly closed/minimized. Also fixes mobile font sizes and avatar positioning.
     401
     402= 1.0.21 =
     403Mobile experience update - Significantly improved mobile responsive design with smaller, more compact chat interface. Recommended for all users with mobile visitors.
     404
     405= 1.0.20 =
     406UX improvements - Better auto-scroll behavior and proper currency formatting. Recommended update.
     407
     408= 1.0.19 =
     409Major mobile responsive update - Chat window and modals now properly sized for mobile devices.
     410
     411= 1.0.14 =
     412WordPress Plugin Check compliance update - Recommended for all users. Includes security improvements and internationalization enhancements.
     413
     414= 1.0.13 =
     415Security update - Please update immediately for improved security and WordPress coding standards compliance.
     416
     417== External Services ==
     418
     419This plugin connects to the ConvertyBot API service (https://convertybot.com) to provide AI-powered chat functionality. When using this plugin:
     420
     421* Product catalog data is securely transmitted for AI processing
     422* Chat conversations are processed through the ConvertyBot AI service
     423* Analytics data is stored to provide dashboard insights
     424
     425No personal data is collected without user consent. The plugin includes a GDPR-compliant consent banner.
     426
     427**Service Provider:** ConvertyBot
     428**Terms of Service:** [https://convertybot.com/terms](https://convertybot.com/terms)
     429**Privacy Policy:** [https://convertybot.com/privacy](https://convertybot.com/privacy)
     430
     431== Support & Community ==
     432
     433* **Website:** [https://convertybot.com/](https://convertybot.com/)
     434* **Documentation:** [https://convertybot.com/docs](https://convertybot.com/docs)
     435* **Support Email:** [email protected]
     436
     437== Why Choose ConvertyBot? ==
     438
     439**💰 Increase Revenue** - Convert more visitors into paying customers
     440**⏰ Save Time** - Let AI handle customer questions 24/7
     441**📈 Grow Faster** - Scale your sales without scaling your team
     442**🎯 Better Targeting** - Personalized recommendations that convert
     443**🛡️ Stay Secure** - GDPR compliant with enterprise security
     444**🚀 Start Free** - No risk, immediate results
Note: See TracChangeset for help on using the changeset viewer.