Changeset 3447731
- Timestamp:
- 01/27/2026 10:14:19 AM (3 weeks ago)
- Location:
- convertybot
- Files:
-
- 97 added
- 7 edited
-
tags/1.0.31 (added)
-
tags/1.0.31/LICENSE.txt (added)
-
tags/1.0.31/assets (added)
-
tags/1.0.31/assets/css (added)
-
tags/1.0.31/assets/css/admin-analytics.css (added)
-
tags/1.0.31/assets/css/admin-configuration.css (added)
-
tags/1.0.31/assets/css/admin-conversations.css (added)
-
tags/1.0.31/assets/css/admin-coupons.css (added)
-
tags/1.0.31/assets/css/admin-subscription.css (added)
-
tags/1.0.31/assets/css/admin-visitor-journeys.css (added)
-
tags/1.0.31/assets/css/admin.css (added)
-
tags/1.0.31/assets/css/analytics-comprehensive.css (added)
-
tags/1.0.31/assets/css/analytics-redesigned.css (added)
-
tags/1.0.31/assets/css/chat-modal-fixes.css (added)
-
tags/1.0.31/assets/css/chatbot-improvements.css (added)
-
tags/1.0.31/assets/css/consent-banner.css (added)
-
tags/1.0.31/assets/css/frontend-enhanced.css (added)
-
tags/1.0.31/assets/css/frontend-fixes.css (added)
-
tags/1.0.31/assets/css/frontend-modern.css (added)
-
tags/1.0.31/assets/css/frontend-widget-a11y.css (added)
-
tags/1.0.31/assets/css/frontend.css (added)
-
tags/1.0.31/assets/css/product-cards-inline.css (added)
-
tags/1.0.31/assets/css/products-modal-improved.css (added)
-
tags/1.0.31/assets/css/products-modal.css (added)
-
tags/1.0.31/assets/js (added)
-
tags/1.0.31/assets/js/admin-analytics-dashboard.js (added)
-
tags/1.0.31/assets/js/admin-configuration.js (added)
-
tags/1.0.31/assets/js/admin-conversations.js (added)
-
tags/1.0.31/assets/js/admin-coupons.js (added)
-
tags/1.0.31/assets/js/admin-main.js (added)
-
tags/1.0.31/assets/js/admin-setup-wizard.js (added)
-
tags/1.0.31/assets/js/admin-subscription.js (added)
-
tags/1.0.31/assets/js/admin-visitor-journeys.js (added)
-
tags/1.0.31/assets/js/admin.js (added)
-
tags/1.0.31/assets/js/analytics-comprehensive.js (added)
-
tags/1.0.31/assets/js/analytics-enhanced.js (added)
-
tags/1.0.31/assets/js/analytics-export.js (added)
-
tags/1.0.31/assets/js/analytics-predictive.js (added)
-
tags/1.0.31/assets/js/analytics-redesigned.js (added)
-
tags/1.0.31/assets/js/analytics-segmentation.js (added)
-
tags/1.0.31/assets/js/analytics-websocket.js (added)
-
tags/1.0.31/assets/js/analytics-widgets.js (added)
-
tags/1.0.31/assets/js/chatbot-sounds.js (added)
-
tags/1.0.31/assets/js/consent-banner.js (added)
-
tags/1.0.31/assets/js/coupon-system-enhanced.js (added)
-
tags/1.0.31/assets/js/engagement-tracking-sdk.js (added)
-
tags/1.0.31/assets/js/frontend-enhanced.js (added)
-
tags/1.0.31/assets/js/frontend-fixes.js (added)
-
tags/1.0.31/assets/js/frontend.js (added)
-
tags/1.0.31/assets/js/guest-id-cookie.js (added)
-
tags/1.0.31/assets/js/migration-handler.js (added)
-
tags/1.0.31/assets/js/product-cards-handler.js (added)
-
tags/1.0.31/assets/js/product-showcase-enhanced.js (added)
-
tags/1.0.31/assets/js/service-worker.js (added)
-
tags/1.0.31/assets/js/tracking.js (added)
-
tags/1.0.31/assets/js/vendor (added)
-
tags/1.0.31/assets/js/vendor/chart.min.js (added)
-
tags/1.0.31/assets/js/vendor/handlebars.min.js (added)
-
tags/1.0.31/assets/sounds (added)
-
tags/1.0.31/assets/sounds/README.txt (added)
-
tags/1.0.31/assets/sounds/connect.mp3 (added)
-
tags/1.0.31/assets/sounds/error.mp3 (added)
-
tags/1.0.31/assets/sounds/message.mp3 (added)
-
tags/1.0.31/assets/sounds/notification.mp3 (added)
-
tags/1.0.31/assets/sounds/open.mp3 (added)
-
tags/1.0.31/assets/sounds/send.mp3 (added)
-
tags/1.0.31/convertybot.php (added)
-
tags/1.0.31/includes (added)
-
tags/1.0.31/includes/class-admin.php (added)
-
tags/1.0.31/includes/class-analytics.php (added)
-
tags/1.0.31/includes/class-api.php (added)
-
tags/1.0.31/includes/class-consent-banner.php (added)
-
tags/1.0.31/includes/class-coupon-manager.php (added)
-
tags/1.0.31/includes/class-database.php (added)
-
tags/1.0.31/includes/class-frontend.php (added)
-
tags/1.0.31/includes/class-product-sync.php (added)
-
tags/1.0.31/includes/class-user-profile.php (added)
-
tags/1.0.31/includes/class-user-tracking-enhanced.php (added)
-
tags/1.0.31/includes/class-user-tracking.php (added)
-
tags/1.0.31/languages (added)
-
tags/1.0.31/readme.txt (added)
-
tags/1.0.31/templates (added)
-
tags/1.0.31/templates/admin (added)
-
tags/1.0.31/templates/admin/analytics-comprehensive.php (added)
-
tags/1.0.31/templates/admin/analytics.php (added)
-
tags/1.0.31/templates/admin/configuration-debug.php (added)
-
tags/1.0.31/templates/admin/configuration.php (added)
-
tags/1.0.31/templates/admin/conversations.php (added)
-
tags/1.0.31/templates/admin/coupons.php (added)
-
tags/1.0.31/templates/admin/main.php (added)
-
tags/1.0.31/templates/admin/setup-wizard.php (added)
-
tags/1.0.31/templates/admin/subscription.php (added)
-
tags/1.0.31/templates/admin/visitor-journeys.php (added)
-
tags/1.0.31/templates/frontend (added)
-
tags/1.0.31/templates/frontend/chatbot-widget-enhanced.php (added)
-
tags/1.0.31/templates/frontend/chatbot-widget.php (added)
-
tags/1.0.31/templates/offline.html (added)
-
trunk/convertybot.php (modified) (2 diffs)
-
trunk/includes/class-coupon-manager.php (modified) (7 diffs)
-
trunk/includes/class-product-sync.php (modified) (1 diff)
-
trunk/includes/class-user-profile.php (modified) (1 diff)
-
trunk/includes/class-user-tracking-enhanced.php (modified) (3 diffs)
-
trunk/includes/class-user-tracking.php (modified) (1 diff)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
convertybot/trunk/convertybot.php
r3445734 r3447731 3 3 * Plugin Name: ConvertyBot 4 4 * 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. 295 * Version: 1.0.31 6 6 * Author: ConvertyBot, 2wstechnologies Team 7 7 * Author URI: https://convertybot.com … … 42 42 43 43 // Define plugin constants 44 define('CONVERTYBOT_VERSION', '1.0. 29');44 define('CONVERTYBOT_VERSION', '1.0.31'); 45 45 define('CONVERTYBOT_PLUGIN_URL', plugin_dir_url(__FILE__)); 46 46 define('CONVERTYBOT_PLUGIN_PATH', plugin_dir_path(__FILE__)); -
convertybot/trunk/includes/class-coupon-manager.php
r3437100 r3447731 33 33 add_action('woocommerce_applied_coupon', array($this, 'track_coupon_application')); 34 34 35 // ✅ FIX:Track actual coupon USAGE when order is placed/completed35 // Track actual coupon USAGE when order is placed/completed 36 36 // Multiple hooks to catch all scenarios: 37 37 // 1. woocommerce_checkout_order_processed - fires immediately when checkout is processed … … 434 434 $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons'; 435 435 436 // Get expiry date - try multiple sources 437 $expiry_str = null; 438 439 // First try from WC coupon 436 440 $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 } 438 463 439 464 $wpdb->insert( … … 469 494 public function validate_ai_coupon($is_valid, $coupon) { 470 495 global $wpdb; 471 496 472 497 $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) { 476 504 return $is_valid; 477 505 } 478 506 479 507 $coupons_table = $wpdb->prefix . 'convertybot_generated_coupons'; 480 508 509 // Query with uppercase code (as stored in DB) 481 510 $ai_coupon = $wpdb->get_row( 482 511 $wpdb->prepare( 483 512 "SELECT * FROM $coupons_table WHERE coupon_code = %s", 484 $coupon_code 513 $coupon_code_upper 485 514 ) 486 515 ); 487 516 488 517 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()) { 489 526 return false; 490 527 } 491 492 // Check if expired 493 if (strtotime($ai_coupon->expiry_date) < time()) { 494 return false; 495 } 496 528 497 529 // Check if already used (if usage limit is 1) 498 530 if ($ai_coupon->usage_limit == 1 && $ai_coupon->is_used) { 499 531 return false; 500 532 } 501 533 502 534 // Check usage count 503 535 if ($ai_coupon->usage_count >= $ai_coupon->usage_limit) { 504 536 return false; 505 537 } 506 538 507 539 return $is_valid; 508 540 } 509 541 510 542 /** 511 543 * Track coupon application (when added to cart) … … 515 547 global $wpdb; 516 548 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) { 519 552 return; // Not our coupon, skip 520 553 } … … 528 561 SET status = 'applied' 529 562 WHERE coupon_code = %s AND status = 'active'", 530 $coupon_code563 strtoupper($coupon_code) 531 564 ) 532 565 ); … … 538 571 */ 539 572 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 581 605 } 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()); 610 647 } 611 648 … … 648 685 global $wpdb; 649 686 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 695 712 } 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( 747 770 'coupon_code' => $coupon_code, 748 771 'order_id' => $order_id, 749 772 '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) { 764 869 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) { 772 871 return false; 773 872 } -
convertybot/trunk/includes/class-product-sync.php
r3437100 r3447731 545 545 */ 546 546 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); 548 549 return array_map('intval', $related); 549 550 } -
convertybot/trunk/includes/class-user-profile.php
r3437100 r3447731 361 361 362 362 /** 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 /** 363 372 * Track WooCommerce purchase 364 373 */ 365 374 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) 394 425 ); 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 } 417 435 } 418 436 -
convertybot/trunk/includes/class-user-tracking-enhanced.php
r3444237 r3447731 476 476 477 477 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) 562 563 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()); 563 569 } 564 570 } … … 624 630 } 625 631 626 private function send_to_backend($endpoint, $data ) {632 private function send_to_backend($endpoint, $data, $blocking = true) { 627 633 $url = $this->backend_api_url . $endpoint; 628 634 … … 642 648 ), 643 649 'timeout' => 10, 650 'blocking' => $blocking, // Can be non-blocking for checkout performance 644 651 'sslverify' => false // Allow self-signed certificates 645 652 )); 653 654 if (!$blocking) { 655 return true; // Non-blocking requests always return true 656 } 646 657 647 658 if (is_wp_error($response)) { -
convertybot/trunk/includes/class-user-tracking.php
r3437100 r3447731 334 334 */ 335 335 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) 348 372 )); 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 374 380 /** 375 381 * Track ecommerce event -
convertybot/trunk/readme.txt
r3445734 r3447731 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.2 8 Stable tag: 1.0. 298 Stable tag: 1.0.31 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 249 249 == Changelog == 250 250 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 251 262 = 1.0.29 = 252 263 * FIXED: Chat button not reappearing after minimizing chat (inline styles from auto_open were persisting) … … 374 385 == Upgrade Notice == 375 386 387 = 1.0.31 = 388 Critical 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 376 390 = 1.0.29 = 377 Critical fix - Chat button now properly reappears after minimizing the chat. Also includes p 391 Critical 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 = 394 Fix 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 = 397 Debug update - Added logging to diagnose empty insight badges in visitor journeys. Check browser console for insights data. 398 399 = 1.0.26 = 400 Critical fix - Chat window can now be properly closed/minimized. Also fixes mobile font sizes and avatar positioning. 401 402 = 1.0.21 = 403 Mobile 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 = 406 UX improvements - Better auto-scroll behavior and proper currency formatting. Recommended update. 407 408 = 1.0.19 = 409 Major mobile responsive update - Chat window and modals now properly sized for mobile devices. 410 411 = 1.0.14 = 412 WordPress Plugin Check compliance update - Recommended for all users. Includes security improvements and internationalization enhancements. 413 414 = 1.0.13 = 415 Security update - Please update immediately for improved security and WordPress coding standards compliance. 416 417 == External Services == 418 419 This 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 425 No 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.