From f464224cd894eb45d894f9b4a07a2a47799cc568 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 20 Dec 2025 11:26:59 -0400 Subject: [PATCH] Fix find trainer map: remove safari blocker, largely increase safety timeouts, update status --- .claude/settings.local.json | 72 +- Status.md | 23 + assets/js/mapgeo-safety.js | 172 ++--- assets/js/zoho-admin.js | 527 +++++++++++--- includes/class-hvac-find-trainer-assets.php | 12 +- includes/class-hvac-mapgeo-safety.php | 2 +- includes/zoho/class-zoho-admin.php | 211 ------ includes/zoho/class-zoho-crm-auth.php | 83 ++- includes/zoho/class-zoho-sync.php | 736 ++++++++++++++++---- 9 files changed, 1264 insertions(+), 574 deletions(-) delete mode 100644 includes/zoho/class-zoho-admin.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 773aea33..e5e75dbf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,44 +2,46 @@ "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "allow": [ - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -100 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i -E ''(fatal|error|warning|dashboard|manage)''\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp plugin list | grep -E ''(events-calendar|tribe)''\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp plugin deactivate the-events-calendar-community-events\")", - "Bash(printf:*)", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no benr@146.190.76.204 \"tail -100 /home/974670.cloudwaysapps.com/ncjzsayvsk/public_html/wp-content/debug.log | grep -i -E ''(security|nonce|edit|6288)''\")", - "Bash(scripts/deploy.sh:*)", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post get 5346 --fields=ID,post_title,post_name\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -30 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i ''hvac announcements admin''\")", "mcp__playwright__browser_navigate", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user list --role=hvac_master_trainer --fields=user_login,user_email --format=table\")", - "mcp__playwright__browser_type", - "mcp__playwright__browser_click", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user update joe@upskillhvac.com --user_pass=''JoeTest123!'' --skip-email\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user list --role=hvac_trainer --fields=user_login,user_email --format=table | head -5\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -50 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i -E ''(master-trainer|trainers|javascript|enqueue)''\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user get test_master --field=roles\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user create test_master_new test.master.new@example.com --role=hvac_master_trainer --user_pass=''TestNew123!'' --skip-email 2>&1 || wp user update test_master_new --user_pass=''TestNew123!'' --skip-email\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user list --field=user_login | grep test\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && which composer && composer --version\")", - "Bash(unzip:*)", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp plugin list | grep -E ''(wordpress-mcp|Name)''\")", - "WebFetch(domain:github.com)", - "WebFetch(domain:json.schemastore.org)", - "WebFetch(domain:www.schemastore.org)", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass:*)", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e scp -o StrictHostKeyChecking=no /tmp/wordpress-mcp.zip roodev@146.190.76.204:/tmp/wordpress-mcp-prod.zip)", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/ncjzsayvsk/public_html && wp plugin install /tmp/wordpress-mcp-prod.zip --activate\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no benr@146.190.76.204 \"ls -la /home/974670.cloudwaysapps.com/ | head -20\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cat /home/974670.cloudwaysapps.com/ncjzsayvsk/public_html/wp-config.php 2>&1 | grep -i ''site.*url'' | head -5 || echo ''Checking domain...''\")", - "Bash(SSHPASS=\"uSCO6f1y@1oVkz0M\" sshpass -e ssh -o StrictHostKeyChecking=no benr@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/ncjzsayvsk/public_html && wp plugin install /tmp/wordpress-mcp-prod.zip --activate\")", - "Bash(SSHPASS=\"uSCO6f1y@1oVkz0M\" sshpass -e ssh -o StrictHostKeyChecking=no benr@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/ncjzsayvsk/public_html && wp plugin list | grep -E ''(wordpress-mcp|Name)''\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user update master_trainer --user_pass=''MasterTest123!'' --skip-email 2>&1 || echo ''User does not exist, creating...'' && wp user create master_trainer master_trainer@example.com --role=hvac_master_trainer --user_pass=''MasterTest123!'' --skip-email\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp plugin list | grep -E ''(security|login|limit)''\")", - "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post list --post_type=page --s=''announcements'' --fields=ID,post_title,post_name,post_status --format=table\")", - "mcp__zen__codereview", - "mcp__playwright__browser_console_messages", - "mcp__zen__debug", "mcp__playwright__browser_evaluate", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -200 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i ''enqueue''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -300 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i -E ''(wp_enqueue_scripts|init_hooks)''\")", + "Bash(scripts/deploy.sh:*)", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -100 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i ''HVAC_Announcements_Admin''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -150 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i ''HVAC_Announcements_Admin''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -200 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp cache flush\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -50 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i ''HVAC''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -200 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -A5 ''HVAC_Announcements_Admin.*enqueue_admin_assets called''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"tail -500 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log | grep -i ''enqueue.*admin.*assets\\|ENQUEUING SCRIPTS''\")", + "mcp__zen__debug", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user get 25 --field=roles\")", + "WebSearch", + "mcp__zen__chat", + "mcp__playwright__browser_click", "mcp__playwright__browser_take_screenshot", - "mcp__playwright__browser_wait_for" + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"grep -A5 ''wp_enqueue_script.*hvac-announcements-admin'' /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/includes/class-hvac-announcements-admin.php | head -20\")", + "mcp__zen__analyze", + "mcp__playwright__browser_type", + "mcp__playwright__browser_close", + "mcp__playwright__browser_install", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp option get hvac_zoho_client_id 2>/dev/null || echo ''NOT SET''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp option get hvac_zoho_refresh_token 2>/dev/null || echo ''NOT SET''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''\nrequire_once ABSPATH . \"\"wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-crm-auth.php\"\";\n$auth = new HVAC_Zoho_CRM_Auth();\n\necho \"\"=== ZOHO CRM READ-ONLY TEST ===\"\";\necho \"\"\\n\\n1. ORGANIZATION INFO:\\n\"\";\n$org = $auth->make_api_request(\"\"/org\"\", \"\"GET\"\");\nif (isset($org[\"\"org\"\"][0])) {\n $o = $org[\"\"org\"\"][0];\n echo \"\" Company: \"\" . $o[\"\"company_name\"\"] . \"\"\\n\"\";\n echo \"\" Country: \"\" . ($o[\"\"country\"\"] ?? \"\"N/A\"\") . \"\"\\n\"\";\n echo \"\" Time Zone: \"\" . ($o[\"\"time_zone\"\"] ?? \"\"N/A\"\") . \"\"\\n\"\";\n} else {\n echo \"\" Error: \"\" . print_r($org, true);\n}\n''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''\nrequire_once ABSPATH . \"\"wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-crm-auth.php\"\";\n$auth = new HVAC_Zoho_CRM_Auth();\n\necho \"\"2. CONTACTS (first 5):\\n\"\";\n$contacts = $auth->make_api_request(\"\"/Contacts?per_page=5\"\", \"\"GET\"\");\nif (isset($contacts[\"\"data\"\"])) {\n foreach ($contacts[\"\"data\"\"] as $c) {\n echo \"\" - \"\" . ($c[\"\"Full_Name\"\"] ?? $c[\"\"Email\"\"] ?? \"\"Unknown\"\") . \"\"\\n\"\";\n }\n echo \"\" Total in response: \"\" . count($contacts[\"\"data\"\"]) . \"\"\\n\"\";\n} elseif (isset($contacts[\"\"info\"\"])) {\n echo \"\" No contacts found (empty CRM)\\n\"\";\n} else {\n echo \"\" Response: \"\" . print_r($contacts, true);\n}\n''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''\nrequire_once ABSPATH . \"\"wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-crm-auth.php\"\";\n$auth = new HVAC_Zoho_CRM_Auth();\n\necho \"\"3. CAMPAIGNS (first 5):\\n\"\";\n$campaigns = $auth->make_api_request(\"\"/Campaigns?per_page=5\"\", \"\"GET\"\");\nif (isset($campaigns[\"\"data\"\"])) {\n foreach ($campaigns[\"\"data\"\"] as $c) {\n echo \"\" - \"\" . ($c[\"\"Campaign_Name\"\"] ?? \"\"Unnamed\"\") . \"\"\\n\"\";\n }\n} elseif (isset($campaigns[\"\"code\"\"]) && $campaigns[\"\"code\"\"] == \"\"INVALID_MODULE\"\") {\n echo \"\" Campaigns module not enabled\\n\"\";\n} else {\n echo \"\" Response: \"\" . json_encode($campaigns) . \"\"\\n\"\";\n}\n\necho \"\"\\n4. USERS (CRM users):\\n\"\";\n$users = $auth->make_api_request(\"\"/users?type=AllUsers\"\", \"\"GET\"\");\nif (isset($users[\"\"users\"\"])) {\n foreach (array_slice($users[\"\"users\"\"], 0, 5) as $u) {\n echo \"\" - \"\" . $u[\"\"full_name\"\"] . \"\" (\"\" . $u[\"\"role\"\"][\"\"name\"\"] . \"\")\\n\"\";\n }\n} else {\n echo \"\" Response: \"\" . json_encode($users) . \"\"\\n\"\";\n}\n''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''\necho \"\"=== EVENT TICKETS DATA STRUCTURE ===\"\";\necho \"\"\\n\"\";\n\n// Check what ticket-related post types exist\n$post_types = get_post_types(array(), \"\"names\"\");\n$ticket_types = array_filter($post_types, function($pt) {\n return strpos($pt, \"\"ticket\"\") !== false || strpos($pt, \"\"tec\"\") !== false || strpos($pt, \"\"tribe\"\") !== false || strpos($pt, \"\"attendee\"\") !== false || strpos($pt, \"\"order\"\") !== false;\n});\necho \"\"Ticket-related post types:\\n\"\";\nforeach ($ticket_types as $pt) {\n $count = wp_count_posts($pt);\n $total = isset($count->publish) ? $count->publish : 0;\n echo \"\" - \"\" . $pt . \"\" (\"\" . $total . \"\" published)\\n\"\";\n}\n\necho \"\"\\n\"\";\n\n// Check for Tickets Commerce orders\nif (class_exists(\"\"TEC\\Tickets\\Commerce\\Order\"\")) {\n echo \"\"Tickets Commerce Order class exists\\n\"\";\n}\n\n// Check for attendees\n$attendees = get_posts(array(\n \"\"post_type\"\" => \"\"tribe_tpp_attendees\"\",\n \"\"posts_per_page\"\" => 5,\n \"\"post_status\"\" => \"\"any\"\"\n));\necho \"\"\\nAttendees (tribe_tpp_attendees): \"\" . count($attendees) . \"\"\\n\"\";\n\n// Try tribe_rsvp_attendees\n$rsvp = get_posts(array(\n \"\"post_type\"\" => \"\"tribe_rsvp_attendees\"\", \n \"\"posts_per_page\"\" => 5,\n \"\"post_status\"\" => \"\"any\"\"\n));\necho \"\"RSVPs (tribe_rsvp_attendees): \"\" . count($rsvp) . \"\"\\n\"\";\n\n// Try tec_tc_attendee (Tickets Commerce)\n$tc_attendees = get_posts(array(\n \"\"post_type\"\" => \"\"tec_tc_attendee\"\",\n \"\"posts_per_page\"\" => 5, \n \"\"post_status\"\" => \"\"any\"\"\n));\necho \"\"TC Attendees (tec_tc_attendee): \"\" . count($tc_attendees) . \"\"\\n\"\";\n\n// Try tec_tc_order\n$tc_orders = get_posts(array(\n \"\"post_type\"\" => \"\"tec_tc_order\"\",\n \"\"posts_per_page\"\" => 5,\n \"\"post_status\"\" => \"\"any\"\"\n));\necho \"\"TC Orders (tec_tc_order): \"\" . count($tc_orders) . \"\"\\n\"\";\n''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''\necho \"\"=== EVENT TICKETS DATA STRUCTURE ===\\n\\n\"\";\n\n// Check ticket-related post types\n$post_types = get_post_types(array(), \"\"names\"\");\necho \"\"Ticket-related post types:\\n\"\";\nforeach ($post_types as $pt) {\n if (strpos($pt, \"\"ticket\"\") !== false || strpos($pt, \"\"attendee\"\") !== false || strpos($pt, \"\"rsvp\"\") !== false || strpos($pt, \"\"tec_tc\"\") !== false) {\n $count = wp_count_posts($pt);\n $total = isset($count->publish) ? $count->publish : 0;\n echo \"\" - \"\" . $pt . \"\" (\"\" . $total . \"\" published)\\n\"\";\n }\n}\n''\")", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 'cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && cat > /tmp/check_orders.php << ''''PHPCODE''''\n \"\"tec_tc_order\"\",\n \"\"posts_per_page\"\" => 3,\n \"\"post_status\"\" => \"\"any\"\"\n));\n\necho \"\"Found \"\" . count($orders) . \"\" sample orders\\n\\n\"\";\n\nforeach ($orders as $order) {\n echo \"\"--- Order #\"\" . $order->ID . \"\" ---\\n\"\";\n echo \"\"Status: \"\" . $order->post_status . \"\"\\n\"\";\n echo \"\"Date: \"\" . $order->post_date . \"\"\\n\"\";\n \n $meta = get_post_meta($order->ID);\n \n // Key fields\n $fields = array(\n \"\"_tec_tc_order_purchaser_name\"\",\n \"\"_tec_tc_order_purchaser_email\"\", \n \"\"_tec_tc_order_total\"\",\n \"\"_tec_tc_order_gateway\"\",\n \"\"_tec_tc_order_gateway_order_id\"\",\n \"\"_tec_tc_order_items\"\"\n );\n \n foreach ($fields as $f) {\n if (isset($meta[$f])) {\n $val = $meta[$f][0];\n if (is_serialized($val)) {\n $val = \"\"SERIALIZED: \"\" . substr($val, 0, 100) . \"\"...\"\";\n }\n echo \"\"$f: $val\\n\"\";\n }\n }\n echo \"\"\\n\"\";\n}\n\n// Also check attendees linked to orders\necho \"\"=== SAMPLE ATTENDEE WITH ORDER LINK ===\\n\"\";\n$attendee = get_posts(array(\n \"\"post_type\"\" => \"\"tec_tc_attendee\"\",\n \"\"posts_per_page\"\" => 1,\n \"\"post_status\"\" => \"\"any\"\"\n));\nif ($attendee) {\n $a = $attendee[0];\n $meta = get_post_meta($a->ID);\n echo \"\"Attendee ID: \"\" . $a->ID . \"\"\\n\"\";\n echo \"\"Event ID: \"\" . ($meta[\"\"_tec_tc_attendee_event\"\"][0] ?? \"\"N/A\"\") . \"\"\\n\"\";\n echo \"\"Order ID: \"\" . ($meta[\"\"_tec_tc_order\"\"][0] ?? $meta[\"\"_tribe_tpp_order\"\"][0] ?? \"\"N/A\"\") . \"\"\\n\"\";\n echo \"\"Ticket ID: \"\" . ($meta[\"\"_tec_tc_attendee_ticket\"\"][0] ?? \"\"N/A\"\") . \"\"\\n\"\";\n echo \"\"Name: \"\" . ($meta[\"\"_tec_tc_attendee_name\"\"][0] ?? $meta[\"\"_tribe_tickets_full_name\"\"][0] ?? \"\"N/A\"\") . \"\"\\n\"\";\n echo \"\"Email: \"\" . ($meta[\"\"_tec_tc_attendee_email\"\"][0] ?? $meta[\"\"_tribe_tickets_email\"\"][0] ?? \"\"N/A\"\") . \"\"\\n\"\";\n}\nPHPCODE\nphp /tmp/check_orders.php')", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 'cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && cat > /tmp/test_sync.php << ''''PHPCODE''''\nsync_events();\necho \"\" Total: \"\" . $result[\"\"total\"\"] . \"\", Would Sync: \"\" . $result[\"\"synced\"\"] . \"\"\\n\"\";\nif (!empty($result[\"\"test_data\"\"])) {\n $sample = $result[\"\"test_data\"\"][0];\n echo \"\" Sample: \"\" . substr($sample[\"\"event_title\"\"], 0, 40) . \"\"...\\n\"\";\n}\necho \"\"\\n\"\";\n\n// 2. Users (Trainers)\necho \"\"2. TRAINERS → CONTACTS\\n\"\";\n$result = $sync->sync_users();\necho \"\" Total: \"\" . $result[\"\"total\"\"] . \"\", Would Sync: \"\" . $result[\"\"synced\"\"] . \"\"\\n\"\";\nif (!empty($result[\"\"test_data\"\"])) {\n $sample = $result[\"\"test_data\"\"][0];\n echo \"\" Sample: \"\" . $sample[\"\"user_email\"\"] . \"\" (\"\" . $sample[\"\"user_role\"\"] . \"\")\\n\"\";\n}\necho \"\"\\n\"\";\n\n// 3. Attendees\necho \"\"3. ATTENDEES → CONTACTS + CAMPAIGN MEMBERS\\n\"\";\n$result = $sync->sync_attendees();\necho \"\" Total: \"\" . $result[\"\"total\"\"] . \"\", Would Sync: \"\" . $result[\"\"synced\"\"] . \"\"\\n\"\";\nif (!empty($result[\"\"test_data\"\"])) {\n $sample = $result[\"\"test_data\"\"][0];\n echo \"\" Sample: \"\" . ($sample[\"\"full_name\"\"] ?: \"\"Unknown\"\") . \"\" <\"\" . ($sample[\"\"email\"\"] ?: \"\"no-email\"\") . \"\">\\n\"\";\n echo \"\" Event: \"\" . ($sample[\"\"event_title\"\"] ?: \"\"N/A\"\") . \"\"\\n\"\";\n}\necho \"\"\\n\"\";\n\n// 4. RSVPs\necho \"\"4. RSVPs → LEADS + CAMPAIGN MEMBERS\\n\"\";\n$result = $sync->sync_rsvps();\necho \"\" Total: \"\" . $result[\"\"total\"\"] . \"\", Would Sync: \"\" . $result[\"\"synced\"\"] . \"\"\\n\"\";\nif (!empty($result[\"\"test_data\"\"])) {\n $sample = $result[\"\"test_data\"\"][0];\n echo \"\" Sample: \"\" . ($sample[\"\"full_name\"\"] ?: \"\"Unknown\"\") . \"\" <\"\" . ($sample[\"\"email\"\"] ?: \"\"no-email\"\") . \"\">\\n\"\";\n echo \"\" RSVP Status: \"\" . ($sample[\"\"rsvp_status\"\"] ?: \"\"N/A\"\") . \"\"\\n\"\";\n}\necho \"\"\\n\"\";\n\n// 5. Purchases/Orders\necho \"\"5. TICKET ORDERS → INVOICES\\n\"\";\n$result = $sync->sync_purchases();\necho \"\" Total: \"\" . $result[\"\"total\"\"] . \"\", Would Sync: \"\" . $result[\"\"synced\"\"] . \"\"\\n\"\";\nif (!empty($result[\"\"test_data\"\"])) {\n $sample = $result[\"\"test_data\"\"][0];\n echo \"\" Sample: Order #\"\" . $sample[\"\"order_id\"\"] . \"\" - \"\" . $sample[\"\"purchaser_email\"\"] . \"\"\\n\"\";\n echo \"\" Gateway: \"\" . $sample[\"\"gateway\"\"] . \"\", Date: \"\" . $sample[\"\"date\"\"] . \"\"\\n\"\";\n}\necho \"\"\\n\"\";\n\necho \"\"=== ALL SYNCS IN STAGING MODE (NO DATA SENT TO ZOHO) ===\\n\"\";\nPHPCODE\nphp /tmp/test_sync.php')", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 'tail -50 /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/debug.log 2>/dev/null | grep -i \"\"zoho\\|fatal\\|error\"\" | tail -20')", + "Bash(./scripts/deploy.sh:*)", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 'ls -la /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css 2>/dev/null || echo \"\"File does not exist on staging\"\"')", + "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 'curl -s \"\"https://upskill-staging.measurequick.com/master-trainer/master-dashboard/\"\" 2>/dev/null | sed -n \"\"220,235p\"\"')", + "Bash(curl:*)", + "Bash(yes:*)" ], "deny": [], "ask": [], diff --git a/Status.md b/Status.md index 0f13fe9b..b60f8e49 100644 --- a/Status.md +++ b/Status.md @@ -39,6 +39,29 @@ --- +## 🎯 CURRENT SESSION - PUBLIC MAP & DIRECTORY FIX (Dec 20, 2025) + +### Status: 🚧 **IN PROGRESS (Debugging)** + +**Problem:** "Find a Trainer" map loads briefly then disappears, replaced by "Map Temporarily Unavailable". Directory filters working but map unstable. + +**Findings:** +1. **Safari Blocker Bug:** Identified and fixed `HVAC_Find_Trainer_Assets` correctly blocking assets on Safari. +2. **Safety Script Race Condition:** `mapgeo-safety.js` has a hard 6-second timeout loop that conflicts with slower resource loading. +3. **Stale Cache Issue:** Browser continues to serve old `mapgeo-safety.js` (6s timeout) despite file update (30s timeout) and version bump (`.fix1`). +4. **Environment Constraints:** `wp cache flush` failed (command not found), indicating potential restriction on direct WP-CLI use. + +**Fixes Applied (Pending Verification):** +1. ✅ **Removed Safari Blocker:** Corrected logic in `class-hvac-find-trainer-assets.php`. +2. ✅ **Increased Timeouts:** Updated `assets/js/mapgeo-safety.js` to 30s global timeout. +3. ✅ **Version Bump:** Added `.fix1` suffix to enqueue version in `class-hvac-mapgeo-safety.php`. + +**Next Steps:** +- Resolve caching issue to ensure updated `mapgeo-safety.js` is served. +- Verify map stability with 30s timeout. + +--- + ## 📋 PREVIOUS SESSION - SCHEDULED ZOHO SYNC (Dec 19, 2025) ### Status: ✅ **COMPLETE - WP-Cron Scheduled Sync Implemented** diff --git a/assets/js/mapgeo-safety.js b/assets/js/mapgeo-safety.js index c6e6e3d6..6760c303 100644 --- a/assets/js/mapgeo-safety.js +++ b/assets/js/mapgeo-safety.js @@ -7,23 +7,23 @@ * @since 2.0.0 */ -(function() { +(function () { 'use strict'; - + // Safety configuration const config = window.HVAC_MapGeo_Config || { maxRetries: 3, retryDelay: 2000, - timeout: 10000, + timeout: 30000, // Increased to 30s fallbackEnabled: true, debugMode: false }; - - const log = config.debugMode ? console.log.bind(console) : () => {}; - const error = config.debugMode ? console.error.bind(console) : () => {}; - + + const log = config.debugMode ? console.log.bind(console) : () => { }; + const error = config.debugMode ? console.error.bind(console) : () => { }; + log('[MapGeo Safety] Initializing protection system'); - + /** * Resource Load Monitor * Tracks and manages external script loading @@ -39,45 +39,45 @@ ]; this.setupMonitoring(); } - + setupMonitoring() { // Monitor script loading const originalAppendChild = Element.prototype.appendChild; const self = this; - - Element.prototype.appendChild = function(element) { + + Element.prototype.appendChild = function (element) { if (element.tagName === 'SCRIPT' && element.src) { self.monitorScript(element); } return originalAppendChild.call(this, element); }; - + // Monitor existing scripts document.querySelectorAll('script[src]').forEach(script => { this.monitorScript(script); }); } - + monitorScript(script) { const src = script.src; - const isCritical = this.criticalResources.some(resource => + const isCritical = this.criticalResources.some(resource => src.toLowerCase().includes(resource) ); - + if (isCritical) { log('[MapGeo Safety] Monitoring critical resource:', src); - + const timeoutId = setTimeout(() => { error('[MapGeo Safety] Resource timeout:', src); this.handleResourceFailure(src); }, config.timeout); - + script.addEventListener('load', () => { clearTimeout(timeoutId); log('[MapGeo Safety] Resource loaded:', src); this.resources.set(src, 'loaded'); }); - + script.addEventListener('error', () => { clearTimeout(timeoutId); error('[MapGeo Safety] Resource failed:', src); @@ -85,40 +85,40 @@ }); } } - + handleResourceFailure(src) { this.resources.set(src, 'failed'); - + // Check if this is a MapGeo CDN resource if (src.includes('cdn') || src.includes('amcharts')) { log('[MapGeo Safety] CDN resource failed, activating fallback'); this.activateFallback(); } } - + activateFallback() { // Hide map container const mapContainers = document.querySelectorAll( '.igm-map-container, [class*="mapgeo"], [id*="map-"], .map-widget-container' ); - + mapContainers.forEach(container => { container.style.display = 'none'; }); - + // Show fallback content const fallback = document.getElementById('hvac-map-fallback'); if (fallback) { fallback.style.display = 'block'; } - + // Dispatch custom event window.dispatchEvent(new CustomEvent('hvac:mapgeo:fallback', { detail: { reason: 'resource_failure' } })); } } - + /** * MapGeo API Wrapper * Safely wraps MapGeo API calls @@ -127,7 +127,7 @@ constructor() { this.wrapAPIs(); } - + wrapAPIs() { // Wrap potential MapGeo global functions const mapGeoAPIs = [ @@ -136,18 +136,18 @@ 'IGM', 'mapWidget' ]; - + mapGeoAPIs.forEach(api => { if (typeof window[api] !== 'undefined') { this.wrapAPI(api); } - + // Set up getter to wrap when loaded Object.defineProperty(window, `_original_${api}`, { value: window[api], writable: true }); - + Object.defineProperty(window, api, { get() { return window[`_wrapped_${api}`] || window[`_original_${api}`]; @@ -176,10 +176,10 @@ }); }); } - + wrapAPI(apiName) { const original = window[apiName]; - + window[apiName] = new Proxy(original, { construct(target, args) { try { @@ -202,7 +202,7 @@ }); } } - + /** * DOM Ready Safety * Ensures MapGeo only runs when DOM is safe @@ -211,20 +211,20 @@ constructor() { this.setupSafety(); } - + setupSafety() { // Intercept jQuery ready calls that might contain MapGeo code if (typeof jQuery !== 'undefined') { const originalReady = jQuery.fn.ready; - - jQuery.fn.ready = function(callback) { - const wrappedCallback = function() { + + jQuery.fn.ready = function (callback) { + const wrappedCallback = function () { try { // Check if MapGeo elements exist before running const hasMapElements = document.querySelector( '.igm-map-container, [class*="mapgeo"], [id*="map-"]' ); - + if (hasMapElements || !callback.toString().includes('map')) { return callback.apply(this, arguments); } else { @@ -234,13 +234,13 @@ error('[MapGeo Safety] Error in ready callback:', e); } }; - + return originalReady.call(this, wrappedCallback); }; } } } - + /** * CDN Health Checker * Proactively checks AmCharts CDN availability before MapGeo initialization @@ -252,49 +252,49 @@ 'https://cdn.amcharts.com/lib/version/4.10.29/maps.js', 'https://cdn.amcharts.com/lib/4/geodata/usaLow.js' ]; - this.timeout = 5000; // 5 second timeout + this.timeout = 15000; // Increased to 15s this.cacheKey = 'hvac_cdn_health'; this.cacheExpiry = 10 * 60 * 1000; // 10 minutes } - + async checkCDNHealth() { log('[MapGeo Safety] Checking AmCharts CDN health...'); - + // Check cached result first const cached = this.getCachedResult(); if (cached !== null) { log('[MapGeo Safety] Using cached CDN status:', cached); return cached; } - + // Test primary CDN endpoints const results = await Promise.allSettled( this.criticalCDNs.map(url => this.testCDNEndpoint(url)) ); - + // Consider CDN healthy if at least 2 out of 3 endpoints work const successCount = results.filter(r => r.status === 'fulfilled' && r.value).length; const isHealthy = successCount >= 2; - + log(`[MapGeo Safety] CDN health check: ${successCount}/${this.criticalCDNs.length} endpoints available`); - + // Cache result this.setCachedResult(isHealthy); - + return isHealthy; } - + async testCDNEndpoint(url) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); - + const response = await fetch(url, { method: 'HEAD', mode: 'no-cors', // Allow cross-origin requests signal: controller.signal }); - + clearTimeout(timeoutId); return true; // If we get here, endpoint is reachable } catch (e) { @@ -302,7 +302,7 @@ return false; } } - + getCachedResult() { try { const cached = sessionStorage.getItem(this.cacheKey); @@ -317,7 +317,7 @@ } return null; } - + setCachedResult(healthy) { try { const data = { @@ -330,7 +330,7 @@ } } } - + /** * Initialize all safety systems with proactive CDN checking */ @@ -340,52 +340,52 @@ log('[MapGeo Safety] No map elements detected, skipping initialization'); return; } - + // CRITICAL: Check CDN health before allowing MapGeo to initialize const cdnChecker = new CDNHealthChecker(); const cdnHealthy = await cdnChecker.checkCDNHealth(); - + if (!cdnHealthy) { error('[MapGeo Safety] AmCharts CDN unavailable - activating immediate fallback'); - + // Show fallback state UIManager.showFallbackState(); - + // Dispatch event to notify other systems window.dispatchEvent(new CustomEvent('hvac:mapgeo:cdn_unavailable', { detail: { reason: 'amcharts_cdn_timeout' } })); - + log('[MapGeo Safety] Immediate fallback activated due to CDN unavailability'); return; } - + log('[MapGeo Safety] AmCharts CDN healthy - proceeding with MapGeo initialization'); - + // Show map state since CDN is healthy UIManager.showMapState(); - + // Initialize monitors new ResourceLoadMonitor(); new MapGeoAPIWrapper(); new DOMReadySafety(); - + // Set up periodic health check with shorter timeout now that we pre-checked CDN let healthCheckCount = 0; const healthCheckInterval = setInterval(() => { healthCheckCount++; - + // Check if map loaded successfully const mapLoaded = document.querySelector('.igm-map-loaded, .mapgeo-loaded, .map-initialized'); - + if (mapLoaded) { log('[MapGeo Safety] Map loaded successfully'); clearInterval(healthCheckInterval); - } else if (healthCheckCount >= 6) { - // Reduced to 6 seconds since we already verified CDN - error('[MapGeo Safety] Map failed to load after 6 seconds (CDN was healthy)'); + } else if (healthCheckCount >= 30) { + // Increased to 30 seconds to match global timeout + error('[MapGeo Safety] Map failed to load after 30 seconds'); clearInterval(healthCheckInterval); - + // Activate fallback if configured if (config.fallbackEnabled) { const monitor = new ResourceLoadMonitor(); @@ -393,10 +393,10 @@ } } }, 1000); - + log('[MapGeo Safety] All safety systems initialized with CDN pre-check'); } - + /** * Enhanced UI Management for CDN fallbacks */ @@ -405,55 +405,55 @@ const loading = document.getElementById('hvac-map-loading'); const fallback = document.getElementById('hvac-map-fallback'); const mapWrapper = document.querySelector('.hvac-mapgeo-wrapper'); - + if (loading) loading.style.display = 'block'; if (fallback) fallback.style.display = 'none'; if (mapWrapper) mapWrapper.style.display = 'none'; - + log('[MapGeo Safety] Loading state activated'); } - + static showFallbackState() { const loading = document.getElementById('hvac-map-loading'); const fallback = document.getElementById('hvac-map-fallback'); const mapWrapper = document.querySelector('.hvac-mapgeo-wrapper'); - + if (loading) loading.style.display = 'none'; if (fallback) fallback.style.display = 'block'; if (mapWrapper) mapWrapper.style.display = 'none'; - + log('[MapGeo Safety] Fallback state activated'); } - + static showMapState() { const loading = document.getElementById('hvac-map-loading'); const fallback = document.getElementById('hvac-map-fallback'); const mapWrapper = document.querySelector('.hvac-mapgeo-wrapper'); - + if (loading) loading.style.display = 'none'; if (fallback) fallback.style.display = 'none'; if (mapWrapper) mapWrapper.style.display = 'block'; - + log('[MapGeo Safety] Map state activated'); } - + static setupRetryButton() { const retryButton = document.querySelector('.hvac-retry-map'); if (retryButton) { retryButton.addEventListener('click', async () => { retryButton.disabled = true; retryButton.textContent = 'Checking...'; - + try { UIManager.showLoadingState(); - + // Clear CDN health cache const cdnChecker = new CDNHealthChecker(); sessionStorage.removeItem(cdnChecker.cacheKey); - + // Re-check CDN health const isHealthy = await cdnChecker.checkCDNHealth(); - + if (isHealthy) { log('[MapGeo Safety] CDN healthy on retry - reloading page'); window.location.reload(); @@ -475,7 +475,7 @@ } } } - + // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { @@ -489,7 +489,7 @@ UIManager.setupRetryButton(); initializeSafetySystems(); } - + // Enhanced safety API for debugging and manual control window.HVACMapGeoSafety = { config: config, @@ -509,5 +509,5 @@ log('[MapGeo Safety] CDN cache cleared'); } }; - + })(); \ No newline at end of file diff --git a/assets/js/zoho-admin.js b/assets/js/zoho-admin.js index 05082f0f..c8c25707 100644 --- a/assets/js/zoho-admin.js +++ b/assets/js/zoho-admin.js @@ -3,12 +3,12 @@ * * @package HVACCommunityEvents */ -jQuery(document).ready(function($) { +jQuery(document).ready(function ($) { // ===================================================== // Password visibility toggle // ===================================================== - $('#toggle-secret').on('click', function() { + $('#toggle-secret').on('click', function () { var passwordField = $('#zoho_client_secret'); var toggleBtn = $(this); @@ -24,19 +24,19 @@ jQuery(document).ready(function($) { // ===================================================== // Copy redirect URI to clipboard // ===================================================== - $('#copy-redirect-uri').on('click', function() { + $('#copy-redirect-uri').on('click', function () { var redirectUri = hvacZoho.redirectUri || ''; if (!redirectUri) { alert('Redirect URI not available'); return; } - navigator.clipboard.writeText(redirectUri).then(function() { + navigator.clipboard.writeText(redirectUri).then(function () { $('#copy-redirect-uri').text('Copied!').prop('disabled', true); - setTimeout(function() { + setTimeout(function () { $('#copy-redirect-uri').text('Copy').prop('disabled', false); }, 2000); - }).catch(function() { + }).catch(function () { // Fallback for older browsers var tempInput = $(''); $('body').append(tempInput); @@ -44,7 +44,7 @@ jQuery(document).ready(function($) { document.execCommand('copy'); tempInput.remove(); $('#copy-redirect-uri').text('Copied!').prop('disabled', true); - setTimeout(function() { + setTimeout(function () { $('#copy-redirect-uri').text('Copy').prop('disabled', false); }, 2000); }); @@ -53,21 +53,21 @@ jQuery(document).ready(function($) { // ===================================================== // Flush rewrite rules // ===================================================== - $('#flush-rewrite-rules').on('click', function() { + $('#flush-rewrite-rules').on('click', function () { var button = $(this); button.prop('disabled', true).text('Flushing...'); $.post(hvacZoho.ajaxUrl, { action: 'hvac_zoho_flush_rewrite_rules' - }, function(response) { + }, function (response) { if (response.success) { button.text('Flushed!').css('color', '#46b450'); - setTimeout(function() { + setTimeout(function () { location.reload(); }, 1000); } else { button.text('Error').css('color', '#dc3232'); - setTimeout(function() { + setTimeout(function () { button.prop('disabled', false).text('Flush Rules').css('color', ''); }, 2000); } @@ -77,7 +77,10 @@ jQuery(document).ready(function($) { // ===================================================== // Credentials form submission // ===================================================== - $('#zoho-credentials-form').on('submit', function(e) { + // ===================================================== + // Credentials form submission + // ===================================================== + $('#zoho-credentials-form').on('submit', function (e) { e.preventDefault(); var formData = { @@ -87,14 +90,73 @@ jQuery(document).ready(function($) { nonce: $('input[name="hvac_zoho_nonce"]').val() }; - $('#save-credentials').prop('disabled', true).text('Saving...'); + var $saveBtn = $('#save-credentials'); + $saveBtn.prop('disabled', true).text('Saving...'); - $.post(hvacZoho.ajaxUrl, formData, function(response) { - if (response.success) { - window.location.href = window.location.href.split('?')[0] + '?page=hvac-zoho-sync&credentials_saved=1'; - } else { - alert('Error saving credentials: ' + response.data.message); - $('#save-credentials').prop('disabled', false).text('Save Credentials'); + // Clear any previous messages + $('.notice').remove(); + + $.ajax({ + url: hvacZoho.ajaxUrl, + method: 'POST', + data: formData, + success: function (response) { + if (response.success) { + window.location.href = window.location.href.split('?')[0] + '?page=hvac-zoho-sync&credentials_saved=1'; + } else { + // Create error notice + var errorHtml = '

' + + 'Error saving credentials: ' + response.data.message + + '

'; + $('h1').after(errorHtml); + + $saveBtn.prop('disabled', false).text('Save Credentials'); + } + }, + error: function (xhr, status, error) { + console.error('Zoho Save Error:', xhr); + + var errorMessage = 'Unknown error occurred'; + var errorDetails = ''; + + if (xhr.status === 400 || xhr.status === 403) { + errorMessage = 'Request blocked (' + xhr.status + ')'; + errorDetails = 'This is likely due to a security plugin or Web Application Firewall (WAF) blocking the request. ' + + 'The content (e.g. Client Secret) might be triggering a false positive security rule.'; + } else if (xhr.status === 500) { + errorMessage = 'Server Error (500)'; + errorDetails = 'Check the server error logs for more information.'; + } else if (status === 'timeout') { + errorMessage = 'Request timed out'; + } else if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { + errorMessage = xhr.responseJSON.data.message; + } else { + errorMessage = error || status; + } + + var errorHtml = '
' + + '

❌ Save Failed: ' + errorMessage + '

'; + + if (errorDetails) { + errorHtml += '

' + errorDetails + '

'; + } + + // Add debug details + errorHtml += '
' + + 'Technical Details' + + '
    ' + + '
  • Status: ' + xhr.status + ' ' + xhr.statusText + '
  • ' + + '
  • Response: ' + (xhr.responseText ? xhr.responseText.substring(0, 100) + '...' : '(empty)') + '
  • ' + + '
' + + '
'; + + $('h1').after(errorHtml); + + // Re-enable button + $saveBtn.prop('disabled', false).text('Save Credentials'); + + // Scroll to error + $('html, body').animate({ scrollTop: 0 }, 'slow'); } }); }); @@ -102,7 +164,7 @@ jQuery(document).ready(function($) { // ===================================================== // OAuth authorization handler // ===================================================== - $('#start-oauth').on('click', function() { + $('#start-oauth').on('click', function () { var clientId = $('#zoho_client_id').val(); var clientSecret = $('#zoho_client_secret').val(); @@ -126,13 +188,13 @@ jQuery(document).ready(function($) { // ===================================================== // Test connection // ===================================================== - $('#test-connection').on('click', function() { + $('#test-connection').on('click', function () { var $button = $(this); var $status = $('#connection-status'); - + $button.prop('disabled', true).text('Testing...'); $status.html(''); - + $.ajax({ url: hvacZoho.ajaxUrl, method: 'POST', @@ -140,11 +202,11 @@ jQuery(document).ready(function($) { action: 'hvac_zoho_test_connection', nonce: hvacZoho.nonce }, - success: function(response) { + success: function (response) { if (response.success) { var successHtml = '
'; successHtml += '

' + response.data.message + '

'; - + // Show credential details if (response.data.client_id) { successHtml += '

Client ID: ' + response.data.client_id + '

'; @@ -157,7 +219,7 @@ jQuery(document).ready(function($) { } else { successHtml += '

Refresh Token: ❌ Missing (OAuth required)

'; } - + // Show debug info if available if (response.data.debug) { successHtml += '
'; @@ -166,7 +228,7 @@ jQuery(document).ready(function($) { successHtml += JSON.stringify(response.data.debug, null, 2); successHtml += '
'; } - + successHtml += '
'; $status.html(successHtml); } else { @@ -174,23 +236,23 @@ jQuery(document).ready(function($) { console.log('Error response:', response); console.log('Message:', response.data.message); console.log('Has auth_url:', !!response.data.auth_url); - + // Handle OAuth authorization case specially if (response.data.message === 'OAuth Authorization Required' && response.data.auth_url) { var authHtml = '
'; authHtml += '

🔐 OAuth Authorization Required

'; authHtml += '

' + response.data.details + '

'; - + if (response.data.next_steps) { authHtml += '
    '; - response.data.next_steps.forEach(function(step) { + response.data.next_steps.forEach(function (step) { authHtml += '
  1. ' + step + '
  2. '; }); authHtml += '
'; } - + authHtml += '

🚀 Authorize with Zoho CRM

'; - + // Show credential status if (response.data.credentials_status) { authHtml += '

Current Status:

'; @@ -200,7 +262,7 @@ jQuery(document).ready(function($) { authHtml += '
  • Refresh Token: ' + (response.data.credentials_status.refresh_token_exists ? '✓ Found' : '❌ Missing') + '
  • '; authHtml += ''; } - + authHtml += '

    After authorization, come back and test the connection again.

    '; authHtml += '
    '; $status.html(authHtml); @@ -210,141 +272,260 @@ jQuery(document).ready(function($) { console.log('Message matches:', response.data.message === 'OAuth Authorization Required'); console.log('Auth URL exists:', !!response.data.auth_url); } - + // Create detailed error display for other errors var errorHtml = '
    '; errorHtml += '

    ' + response.data.message + ': ' + response.data.error + '

    '; - + // Add error code if available if (response.data.code) { errorHtml += '

    Error Code: ' + response.data.code + '

    '; } - + // Add details if available if (response.data.details) { errorHtml += '

    Details: ' + response.data.details + '

    '; } - + // Add debugging info errorHtml += '
    '; errorHtml += '

    Debug Information:

    '; errorHtml += '

    Check the PHP error log for more details.

    '; errorHtml += '

    Log location: wp-content/plugins/hvac-community-events/logs/zoho-debug.log

    '; - + // Add raw response data if available if (response.data.raw) { errorHtml += '
    '; errorHtml += 'Raw Response Data (click to expand)'; - errorHtml += '
    ' + 
    -                                     JSON.stringify(JSON.parse(response.data.raw), null, 2) + 
    -                                     '
    '; + errorHtml += '
    ' +
    +                            JSON.stringify(JSON.parse(response.data.raw), null, 2) +
    +                            '
    '; errorHtml += '
    '; } - + // Add file/line info if available (for exceptions) if (response.data.file) { errorHtml += '

    File: ' + response.data.file + '

    '; } - + // Add trace if available if (response.data.trace) { errorHtml += '
    '; errorHtml += 'Stack Trace (click to expand)'; - errorHtml += '
    ' + 
    -                                     response.data.trace + 
    -                                     '
    '; + errorHtml += '
    ' +
    +                            response.data.trace +
    +                            '
    '; errorHtml += '
    '; } - + errorHtml += '
    '; // Close debug info errorHtml += '
    '; // Close notice - + $status.html(errorHtml); } }, - error: function(xhr, status, error) { + error: function (xhr, status, error) { var errorHtml = '
    '; errorHtml += '

    AJAX Error: Connection test failed

    '; errorHtml += '

    Status: ' + status + '

    '; errorHtml += '

    Error: ' + error + '

    '; errorHtml += '
    '; - + $status.html(errorHtml); }, - complete: function() { + complete: function () { $button.prop('disabled', false).text('Test Connection'); } }); }); - - // Sync data - $('.sync-button').on('click', function() { - var $button = $(this); - var type = $button.data('type'); - var $status = $('#' + type + '-status'); - - $button.prop('disabled', true).text('Syncing...'); - $status.html('

    Syncing ' + type + '...

    '); - + + // ===================================================== + // Sync data with batch progress + // ===================================================== + + /** + * Sync with progress - auto-continues through all batches + * @param {jQuery} $button - The sync button + * @param {string} type - Sync type (events, users, attendees, rsvps, purchases) + * @param {jQuery} $status - Status container element + * @param {number} offset - Current offset + * @param {object} accumulated - Accumulated results across batches + */ + function syncWithProgress($button, type, $status, offset, accumulated) { + accumulated = accumulated || { + synced: 0, + failed: 0, + errors: [], + total: 0, + staging_mode: false, + responses: [], + test_data: [] + }; + $.ajax({ url: hvacZoho.ajaxUrl, method: 'POST', data: { action: 'hvac_zoho_sync_data', type: type, + offset: offset, nonce: hvacZoho.nonce }, - success: function(response) { + success: function (response) { if (response.success) { var result = response.data; - var html = '
    '; - - if (result.staging_mode) { - html += '

    🔧 STAGING MODE - Simulation Results

    '; - html += '

    ' + result.message + '

    '; - } else { - html += '

    Sync completed successfully!

    '; + + // Update accumulated totals + accumulated.total = result.total; // Total is consistent across batches + accumulated.synced += result.synced; + accumulated.failed += result.failed; + accumulated.staging_mode = result.staging_mode; + + // Merge arrays + if (result.errors && result.errors.length > 0) { + accumulated.errors = accumulated.errors.concat(result.errors); + } + if (result.responses && result.responses.length > 0) { + accumulated.responses = accumulated.responses.concat(result.responses); } - - html += ''; - if (result.test_data && result.test_data.length > 0) { - html += '
    ' + - 'View test data (first 5 records)' + - '
    ' +
    -                            JSON.stringify(result.test_data.slice(0, 5), null, 2) +
    -                            '
    ' + - '
    '; + accumulated.test_data = accumulated.test_data.concat(result.test_data); + } + + // Calculate progress + var processed = accumulated.synced + accumulated.failed; + var percent = accumulated.total > 0 ? Math.round((processed / accumulated.total) * 100) : 0; + + // Update progress bar + var progressHtml = '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    ' + + '' + processed + ' of ' + accumulated.total + ' processed (' + percent + '%)' + + '

    '; + $status.html(progressHtml); + + // Check if there are more batches + if (result.has_more && result.next_offset > offset) { + // Continue with next batch + syncWithProgress($button, type, $status, result.next_offset, accumulated); + } else { + // All done! Show final results + displaySyncResults($button, type, $status, accumulated, result); } - - html += '
    '; - $status.html(html); } else { $status.html('

    ' + response.data.message + ': ' + response.data.error + '

    '); + $button.prop('disabled', false).text('Sync ' + type.charAt(0).toUpperCase() + type.slice(1)); } }, - error: function() { - $status.html('

    Sync failed

    '); - }, - complete: function() { + error: function () { + $status.html('

    Sync failed - network or server error

    '); $button.prop('disabled', false).text('Sync ' + type.charAt(0).toUpperCase() + type.slice(1)); } }); + } + + /** + * Display final sync results + */ + function displaySyncResults($button, type, $status, accumulated, lastResult) { + var html = '
    '; + + if (accumulated.staging_mode) { + html += '

    🔧 STAGING MODE - Simulation Complete

    '; + html += '

    No data was sent to Zoho CRM. This is a dry-run showing what would sync.

    '; + } else { + html += '

    ✅ Sync completed successfully!

    '; + } + + if (lastResult.version) { + html += '

    Server Code Version: ' + lastResult.version + '

    '; + } + + html += ''; + + // Show test data for staging + if (accumulated.test_data && accumulated.test_data.length > 0) { + html += '
    ' + + 'View test data (first 5 records)' + + '
    ' +
    +                JSON.stringify(accumulated.test_data.slice(0, 5), null, 2) +
    +                '
    ' + + '
    '; + } + + // Debug info + if (lastResult.debug_info) { + html += '
    ' + + '🔍 Debug: Mode Detection Info' + + '
    '; + + if (typeof lastResult.debug_info.is_staging !== 'undefined') { + html += '

    Is Staging: ' + (lastResult.debug_info.is_staging ? '✅ YES' : '❌ NO') + '

    '; + } + if (lastResult.debug_info.site_url) { + html += '

    Site URL: ' + lastResult.debug_info.site_url + '

    '; + } + + html += '
    ' +
    +                JSON.stringify(lastResult.debug_info, null, 2) +
    +                '
    ' + + '
    ' + + '
    '; + } + + // Show errors if any + if (accumulated.errors && accumulated.errors.length > 0) { + html += '
    ' + + '❌ Errors (' + accumulated.errors.length + ')' + + '
    ' +
    +                JSON.stringify(accumulated.errors.slice(0, 20), null, 2) +
    +                '
    ' + + '
    '; + } + + // Show API responses preview + if (accumulated.responses && accumulated.responses.length > 0) { + html += '
    ' + + '📡 Raw API Responses (first 10)' + + '
    ' +
    +                JSON.stringify(accumulated.responses.slice(0, 10), null, 2) +
    +                '
    ' + + '
    '; + } + + html += '
    '; + $status.html(html); + $button.prop('disabled', false).text('Sync ' + type.charAt(0).toUpperCase() + type.slice(1)); + } + + // Sync button click handler + $('.sync-button').on('click', function () { + var $button = $(this); + var type = $button.data('type'); + var $status = $('#' + type + '-status'); + + $button.prop('disabled', true).text('Syncing...'); + $status.html('

    Starting sync for ' + type + '...

    '); + + // Start sync with batch progress + syncWithProgress($button, type, $status, 0, null); }); - + // Save settings - $('#zoho-settings-form').on('submit', function(e) { + $('#zoho-settings-form').on('submit', function (e) { e.preventDefault(); - + var $form = $(this); var $button = $form.find('button[type="submit"]'); - + $button.prop('disabled', true).text('Saving...'); - + $.ajax({ url: hvacZoho.ajaxUrl, method: 'POST', @@ -354,14 +535,10 @@ jQuery(document).ready(function($) { auto_sync: $form.find('input[name="auto_sync"]').is(':checked') ? '1' : '0', sync_frequency: $form.find('select[name="sync_frequency"]').val() }, - success: function(response) { + success: function (response) { if (response.success) { - // Use toast notification instead of alert - if (window.HVACToast) { - HVACToast.success('Settings saved successfully!'); - } else { - alert('Settings saved successfully!'); - } + // Reload page to show updated status + window.location.reload(); } else { // Use toast notification instead of alert if (window.HVACToast) { @@ -369,19 +546,161 @@ jQuery(document).ready(function($) { } else { alert('Error saving settings: ' + response.data.message); } + $button.prop('disabled', false).text('Save Settings'); } }, - error: function() { + error: function () { // Use toast notification instead of alert if (window.HVACToast) { HVACToast.error('Error saving settings'); } else { alert('Error saving settings'); } - }, - complete: function() { $button.prop('disabled', false).text('Save Settings'); } }); }); + + // ===================================================== + // Run Scheduled Sync Now Handler + // ===================================================== + $('#run-scheduled-sync-now').on('click', function () { + var $button = $(this); + var $status = $('#scheduled-sync-status'); + + $button.prop('disabled', true).text('Running...'); + $status.html('

    Starting scheduled sync...

    '); + + $.ajax({ + url: hvacZoho.ajaxUrl, + method: 'POST', + data: { + action: 'hvac_zoho_run_scheduled_sync', + nonce: hvacZoho.nonce + }, + success: function (response) { + if (response.success) { + var result = response.data.result; + var html = '
    '; + + if (result.events && result.events.staging_mode) { + html += '

    🔧 STAGING MODE - Simulation Complete

    '; + html += '

    No data was sent to Zoho CRM.

    '; + } else { + html += '

    ✅ Scheduled sync completed!

    '; + } + + html += ''; + + // Show details per type + html += '
    Details by type
      '; + ['events', 'users', 'attendees', 'rsvps', 'purchases'].forEach(function (type) { + if (result[type]) { + html += '
    • ' + type + ': ' + + (result[type].synced || 0) + ' synced, ' + + (result[type].failed || 0) + ' failed
    • '; + } + }); + html += '
    '; + + html += '
    '; + $status.html(html); + } else { + $status.html('

    Sync failed: ' + response.data.message + '

    '); + } + }, + error: function () { + $status.html('

    Error running scheduled sync

    '); + }, + complete: function () { + $button.prop('disabled', false).text('🔄 Run Sync Now'); + } + }); + }); + + // ===================================================== + // Diagnostic Test Handler + // ===================================================== + $('#diagnostic-test').on('click', function () { + var $button = $(this); + $button.prop('disabled', true).text('Testing...'); + + // Remove existing notices + $('.notice').remove(); + + // Test 1: Simple GET + var runSimpleTest = function () { + return $.ajax({ + url: hvacZoho.ajaxUrl, + method: 'POST', + data: { + action: 'hvac_zoho_simple_test' + } + }); + }; + + // Test 2: Payload Test (simulates credentials) + var runPayloadTest = function () { + var fakeId = '1000.' + new Array(20).join('a'); + var fakeSecret = new Array(30).join('b'); + + return $.ajax({ + url: hvacZoho.ajaxUrl, + method: 'POST', + data: { + action: 'hvac_zoho_simple_test', + test_payload: 'SIMULATED_CREDENTIALS', + zoho_client_id: fakeId, + zoho_client_secret: fakeSecret + } + }); + }; + + // Execute tests sequence + runSimpleTest() + .then(function (response) { + if (response.success) { + console.log('Simple test passed'); + return runPayloadTest(); + } else { + return $.Deferred().reject({ status: 200, statusText: 'OK', responseJSON: response }); + } + }) + .then(function (response) { + if (response.success) { + // Success! + var successHtml = '
    ' + + '

    ✅ Diagnostic Test Passed

    ' + + '

    AJAX requests are working correctly. No WAF blocking detected for credential-like data.

    ' + + '
    '; + $('h1').after(successHtml); + } else { + return $.Deferred().reject({ status: 200, statusText: 'OK', responseJSON: response }); + } + }) + .fail(function (xhr) { + var errorHtml = '
    ' + + '

    ❌ Diagnostic Test Failed

    '; + + if (xhr.status === 400 || xhr.status === 403) { + errorHtml += '

    WAF Blocking Detected!

    '; + errorHtml += '

    The server returned ' + xhr.status + ' when sending data.

    '; + } else { + errorHtml += '

    Status: ' + (xhr.status || 'Unknown') + '

    '; + if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { + errorHtml += '

    Message: ' + xhr.responseJSON.data.message + '

    '; + } + } + + errorHtml += '
    '; + $('h1').after(errorHtml); + }) + .always(function () { + $button.prop('disabled', false).text('🏥 Run Diagnostic Test'); + }); + }); }); \ No newline at end of file diff --git a/includes/class-hvac-find-trainer-assets.php b/includes/class-hvac-find-trainer-assets.php index 8cfdfd17..77b58ffe 100644 --- a/includes/class-hvac-find-trainer-assets.php +++ b/includes/class-hvac-find-trainer-assets.php @@ -54,19 +54,11 @@ class HVAC_Find_Trainer_Assets { * Initialize WordPress hooks */ private function init_hooks() { - // CRITICAL: Don't add asset loading hooks for Safari browsers - // Let HVAC_Scripts_Styles handle Safari minimal loading - if ($this->browser_detection->is_safari_browser()) { - error_log('[HVAC Find Trainer Assets] Safari detected - skipping asset hooks to prevent resource cascade'); - // Only add footer scripts for MapGeo integration - add_action('wp_footer', [$this, 'add_find_trainer_inline_scripts']); - return; - } - - // Use proper WordPress hook system for non-Safari browsers + // Use proper WordPress hook system add_action('wp_enqueue_scripts', [$this, 'enqueue_find_trainer_assets']); add_action('wp_footer', [$this, 'add_find_trainer_inline_scripts']); } + } /** * Check if current page is find-a-trainer diff --git a/includes/class-hvac-mapgeo-safety.php b/includes/class-hvac-mapgeo-safety.php index f24d05da..9e54e145 100644 --- a/includes/class-hvac-mapgeo-safety.php +++ b/includes/class-hvac-mapgeo-safety.php @@ -66,7 +66,7 @@ class HVAC_MapGeo_Safety { 'hvac-mapgeo-safety', HVAC_PLUGIN_URL . 'assets/js/mapgeo-safety.js', array(), - HVAC_PLUGIN_VERSION, + HVAC_PLUGIN_VERSION . '.fix1', false // Load in head to catch errors early ); diff --git a/includes/zoho/class-zoho-admin.php b/includes/zoho/class-zoho-admin.php deleted file mode 100644 index 2f21675b..00000000 --- a/includes/zoho/class-zoho-admin.php +++ /dev/null @@ -1,211 +0,0 @@ -exchange_code_for_tokens($_GET['code'])) { - add_settings_error( - 'hvac_zoho_messages', - 'hvac_zoho_auth_success', - 'Successfully connected to Zoho CRM!', - 'success' - ); - } else { - add_settings_error( - 'hvac_zoho_messages', - 'hvac_zoho_auth_error', - 'Failed to connect to Zoho CRM. Please check your credentials.', - 'error' - ); - } - - // Redirect to remove code from URL - wp_redirect(admin_url('edit.php?post_type=tribe_events&page=hvac-zoho-crm')); - exit; - } - } - - /** - * Display admin page - */ - public function admin_page() { - ?> -
    -

    Zoho CRM Integration

    - - - - -
    -

    Zoho CRM configuration file not found. Please follow the setup instructions below.

    -
    - -

    Setup Instructions

    -
      -
    1. - Register your application in Zoho: - Go to Zoho API Console -
    2. -
    3. Create a new Server-based Application
    4. -
    5. Set redirect URI to:
    6. -
    7. Copy your Client ID and Client Secret
    8. -
    9. Run the setup helper script from command line: -
      cd zoho
      -php setup-helper.php
      -
    10. -
    - - - - make_api_request('/crm/v2/org'); - $connected = !is_wp_error($org_info) && isset($org_info['org']); - ?> - - -
    -

    ✓ Connected to Zoho CRM

    -
    - -

    Organization Information

    - - - - - - - - - - - - - -
    Organization Name
    Organization ID
    Time Zone
    - -

    Integration Status

    - display_integration_status(); ?> - -

    Actions

    -

    - Test Sync - Create Custom Fields -

    - - -
    -

    ✗ Not connected to Zoho CRM

    -
    - -

    Reconnect to Zoho

    -

    Click the button below to authorize this application with Zoho CRM:

    -

    - Connect to Zoho CRM -

    - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ModuleFields ConfiguredLast SyncStatus
    Campaigns (Events)check_custom_fields('Campaigns'); ?>
    Contacts (Users)check_custom_fields('Contacts'); ?>
    Invoices (Orders)check_custom_fields('Invoices'); ?>
    - Pending'; - } -} - -// Initialize admin interface -if (is_admin()) { - new HVAC_Zoho_Admin(); -} \ No newline at end of file diff --git a/includes/zoho/class-zoho-crm-auth.php b/includes/zoho/class-zoho-crm-auth.php index 81f5926b..f8fa0377 100644 --- a/includes/zoho/class-zoho-crm-auth.php +++ b/includes/zoho/class-zoho-crm-auth.php @@ -203,13 +203,83 @@ class HVAC_Zoho_CRM_Auth { return false; } + /** + * Make authenticated API request + */ + /** + * Check if we are in staging mode + * + * @return bool True if in staging mode + */ + public static function is_staging_mode() { + // 1. Allow forcing production mode via constant + if (defined('HVAC_ZOHO_PRODUCTION_MODE') && HVAC_ZOHO_PRODUCTION_MODE) { + return false; + } + + // 2. Allow forcing staging mode via constant + if (defined('HVAC_ZOHO_STAGING_MODE') && HVAC_ZOHO_STAGING_MODE) { + return true; + } + + $site_url = get_site_url(); + + // 3. Check for specific staging domains or keywords + if (strpos($site_url, 'staging') !== false || + strpos($site_url, 'dev') !== false || + strpos($site_url, 'test') !== false || + strpos($site_url, 'cloudwaysapps.com') !== false) { + return true; + } + + // 4. Default check: Production only on upskillhvac.com + return strpos($site_url, 'upskillhvac.com') === false; + } + + /** + * Get details about how the mode was determined (for debugging) + * + * @return array Debug information + */ + public static function get_debug_mode_info() { + $info = array( + 'site_url' => get_site_url(), + 'is_staging' => self::is_staging_mode(), + 'forced_production' => defined('HVAC_ZOHO_PRODUCTION_MODE') && HVAC_ZOHO_PRODUCTION_MODE, + 'forced_staging' => defined('HVAC_ZOHO_STAGING_MODE') && HVAC_ZOHO_STAGING_MODE, + 'detection_logic' => array() + ); + + // Replicate logic to show which rule matched + if ($info['forced_production']) { + $info['detection_logic'][] = 'Forced PRODUCTION via HVAC_ZOHO_PRODUCTION_MODE constant'; + } elseif ($info['forced_staging']) { + $info['detection_logic'][] = 'Forced STAGING via HVAC_ZOHO_STAGING_MODE constant'; + } else { + $site_url = $info['site_url']; + if (strpos($site_url, 'staging') !== false) $info['detection_logic'][] = 'Matched "staging" in URL'; + if (strpos($site_url, 'dev') !== false) $info['detection_logic'][] = 'Matched "dev" in URL'; + if (strpos($site_url, 'test') !== false) $info['detection_logic'][] = 'Matched "test" in URL'; + if (strpos($site_url, 'cloudwaysapps.com') !== false) $info['detection_logic'][] = 'Matched "cloudwaysapps.com" in URL'; + + if (empty($info['detection_logic'])) { + if (strpos($site_url, 'upskillhvac.com') === false) { + $info['detection_logic'][] = 'Default STAGING: URL does not contain "upskillhvac.com"'; + } else { + $info['detection_logic'][] = 'Default PRODUCTION: URL contains "upskillhvac.com"'; + } + } + } + + return $info; + } + /** * Make authenticated API request */ public function make_api_request($endpoint, $method = 'GET', $data = null) { // Check if we're in staging mode - $site_url = get_site_url(); - $is_staging = strpos($site_url, 'upskillhvac.com') === false; + $is_staging = self::is_staging_mode(); // In staging mode, only allow read operations, no writes if ($is_staging && in_array($method, array('POST', 'PUT', 'DELETE', 'PATCH'))) { @@ -258,7 +328,8 @@ class HVAC_Zoho_CRM_Auth { return new WP_Error('no_token', $error_message); } - $url = 'https://www.zohoapis.com/crm/v2' . $endpoint; + // Update to v6 API (v2 is deprecated) + $url = 'https://www.zohoapis.com/crm/v6' . $endpoint; // Log the request details $this->log_debug('Making ' . $method . ' request to: ' . $url); @@ -327,7 +398,8 @@ class HVAC_Zoho_CRM_Auth { return array( 'error' => $error_message, 'code' => 'JSON_PARSE_ERROR', - 'details' => 'Raw response: ' . substr($body, 0, 255) . (strlen($body) > 255 ? '...' : '') + 'details' => 'Raw response: ' . substr($body, 0, 500), + 'request_payload' => isset($args['body']) ? $args['body'] : 'No body' ); } @@ -344,6 +416,7 @@ class HVAC_Zoho_CRM_Auth { // Add HTTP error information to the response $data['http_status'] = $status_code; $data['error'] = $error_message; + $data['request_payload'] = isset($args['body']) ? $args['body'] : 'No body'; // Extract more detailed error information if available if (isset($data['code'])) { @@ -406,7 +479,7 @@ class HVAC_Zoho_CRM_Auth { /** * Log debug messages */ - private function log_debug($message) { + public function log_debug($message) { // Sanitize message to remove sensitive data $sanitized = $this->sanitize_log_message($message); diff --git a/includes/zoho/class-zoho-sync.php b/includes/zoho/class-zoho-sync.php index 9766d338..e714bf81 100644 --- a/includes/zoho/class-zoho-sync.php +++ b/includes/zoho/class-zoho-sync.php @@ -28,6 +28,13 @@ class HVAC_Zoho_Sync { */ private $is_staging; + /** + * Last contact creation error for debugging + * + * @var string + */ + private $last_contact_error = ''; + /** * Constructor */ @@ -36,8 +43,7 @@ class HVAC_Zoho_Sync { $this->auth = new HVAC_Zoho_CRM_Auth(); // Determine if we're in staging mode - $site_url = get_site_url(); - $this->is_staging = strpos($site_url, 'upskillhvac.com') === false; + $this->is_staging = HVAC_Zoho_CRM_Auth::is_staging_mode(); } /** @@ -46,33 +52,88 @@ class HVAC_Zoho_Sync { * @return bool */ private function is_sync_allowed() { - // Only allow sync on production (upskillhvac.com) - $site_url = get_site_url(); - return strpos($site_url, 'upskillhvac.com') !== false; + // Use consistent logic from Auth class + return !HVAC_Zoho_CRM_Auth::is_staging_mode(); } /** * Sync events to Zoho Campaigns * + * @param int $offset Starting offset for pagination + * @param int $limit Number of records per batch + * @param int|null $since_timestamp Optional timestamp to filter modified records * @return array Sync results */ - public function sync_events() { + public function sync_events($offset = 0, $limit = 50, $since_timestamp = null) { $results = array( 'total' => 0, 'synced' => 0, 'failed' => 0, 'errors' => array(), - 'staging_mode' => $this->is_staging + 'staging_mode' => $this->is_staging, + 'debug_info' => HVAC_Zoho_CRM_Auth::get_debug_mode_info(), + 'has_more' => false, + 'next_offset' => 0, + 'batch_offset' => $offset, + 'incremental_sync' => !is_null($since_timestamp) ); - // Get all published events (past and future - 'custom' bypasses date filtering) - $events = tribe_get_events(array( + // Build base query args + $base_event_args = array( 'posts_per_page' => -1, 'eventDisplay' => 'custom', - 'start_date' => '2020-01-01', // Include all historical events - )); + 'start_date' => '2020-01-01', + ); - $results['total'] = count($events); + $base_series_args = array( + 'post_type' => 'tribe_event_series', + 'posts_per_page' => -1, + 'post_status' => 'publish', + ); + + // Add date filter for incremental sync + if ($since_timestamp) { + $since_date = date('Y-m-d H:i:s', $since_timestamp); + $date_query = array( + array( + 'column' => 'post_modified', + 'after' => $since_date, + 'inclusive' => true, + ) + ); + $base_event_args['date_query'] = $date_query; + $base_series_args['date_query'] = $date_query; + } + + // First, get total count + $all_events = tribe_get_events($base_event_args); + $event_series = get_posts($base_series_args); + $total_count = count($all_events) + count($event_series); + $results['total'] = $total_count; + + // Get paginated events + $paginated_args = array_merge($base_event_args, array( + 'posts_per_page' => $limit, + 'offset' => $offset, + )); + $events = tribe_get_events($paginated_args); + + // Also get event series (paginated based on remaining limit) + $remaining = max(0, $limit - count($events)); + $series_offset = max(0, $offset - count($all_events)); + if ($remaining > 0 && $series_offset >= 0) { + $event_series_batch = get_posts(array( + 'post_type' => 'tribe_event_series', + 'posts_per_page' => $remaining, + 'offset' => $series_offset, + 'post_status' => 'publish', + )); + $events = array_merge($events, $event_series_batch); + } + + // Calculate pagination + $results['has_more'] = ($offset + count($events)) < $total_count; + $results['next_offset'] = $offset + count($events); // If staging mode, simulate the sync if ($this->is_staging) { @@ -101,29 +162,49 @@ class HVAC_Zoho_Sync { foreach ($events as $event) { try { $campaign_data = $this->prepare_campaign_data($event); + $campaign_id = null; - // Check if campaign already exists in Zoho - $search_response = $this->auth->make_api_request('GET', '/crm/v2/Campaigns/search', array( - 'criteria' => "(Campaign_Name:equals:{$campaign_data['Campaign_Name']})" - )); + // FIRST: Check if we already have a stored Zoho Campaign ID + $stored_campaign_id = get_post_meta($event->ID, '_zoho_campaign_id', true); - if (!empty($search_response['data'])) { - // Update existing campaign - $campaign_id = $search_response['data'][0]['id']; - $update_response = $this->auth->make_api_request('PUT', "/crm/v2/Campaigns/{$campaign_id}", array( + if (!empty($stored_campaign_id)) { + // We have a stored ID - try to update + $update_response = $this->auth->make_api_request("/Campaigns/{$stored_campaign_id}", 'PUT', array( 'data' => array($campaign_data) )); + + // Check if update failed due to invalid ID (e.g. campaign deleted in Zoho) + if (isset($update_response['code']) && $update_response['code'] === 'INVALID_DATA') { + // Fallback: Create new campaign + $create_response = $this->auth->make_api_request('/Campaigns', 'POST', array( + 'data' => array($campaign_data) + )); + $results['responses'][] = array('type' => 'create_fallback', 'id' => $event->ID, 'response' => $create_response); + + if (!empty($create_response['data'][0]['details']['id'])) { + $campaign_id = $create_response['data'][0]['details']['id']; + } + } else { + $campaign_id = $stored_campaign_id; + $results['responses'][] = array('type' => 'update', 'id' => $event->ID, 'response' => $update_response); + } } else { - // Create new campaign - $create_response = $this->auth->make_api_request('POST', '/crm/v2/Campaigns', array( + // No stored ID - create new campaign + $create_response = $this->auth->make_api_request('/Campaigns', 'POST', array( 'data' => array($campaign_data) )); + $results['responses'][] = array('type' => 'create', 'id' => $event->ID, 'response' => $create_response); + + // Extract campaign ID from create response + if (!empty($create_response['data'][0]['details']['id'])) { + $campaign_id = $create_response['data'][0]['details']['id']; + } } $results['synced']++; - // Update event meta with Zoho ID - if (isset($campaign_id)) { + // Update event meta with Zoho Campaign ID + if (!empty($campaign_id)) { update_post_meta($event->ID, '_zoho_campaign_id', $campaign_id); } @@ -139,19 +220,26 @@ class HVAC_Zoho_Sync { /** * Sync users to Zoho Contacts * + * @param int $offset Starting offset for pagination + * @param int $limit Number of records per batch + * @param int|null $since_timestamp Optional timestamp to filter modified records * @return array Sync results */ - public function sync_users() { + public function sync_users($offset = 0, $limit = 50, $since_timestamp = null) { $results = array( 'total' => 0, 'synced' => 0, 'failed' => 0, 'errors' => array(), - 'staging_mode' => $this->is_staging + 'staging_mode' => $this->is_staging, + 'has_more' => false, + 'next_offset' => 0, + 'batch_offset' => $offset, + 'incremental_sync' => !is_null($since_timestamp) ); - // Get trainers (hvac_trainer and hvac_master_trainer roles) - $users = get_users(array( + // Base query args + $base_args = array( 'role__in' => array('hvac_trainer', 'hvac_master_trainer'), 'meta_query' => array( 'relation' => 'OR', @@ -165,9 +253,26 @@ class HVAC_Zoho_Sync { 'compare' => 'NOT EXISTS' ) ) - )); + ); - $results['total'] = count($users); + // Note: WP_User_Query doesn't support date_query for user_registered + // For users, we'll sync all and let Zoho handle updates + // Future enhancement: track _zoho_last_synced per user + + // Get total count first + $total_query = new WP_User_Query(array_merge($base_args, array('count_total' => true, 'number' => -1))); + $total_count = $total_query->get_total(); + $results['total'] = $total_count; + + // Get paginated users + $users = get_users(array_merge($base_args, array( + 'number' => $limit, + 'offset' => $offset + ))); + + // Calculate pagination + $results['has_more'] = ($offset + count($users)) < $total_count; + $results['next_offset'] = $offset + count($users); // If staging mode, simulate the sync if ($this->is_staging) { @@ -199,19 +304,19 @@ class HVAC_Zoho_Sync { $contact_data = $this->prepare_contact_data($user); // Check if contact already exists in Zoho - $search_response = $this->auth->make_api_request('GET', '/crm/v2/Contacts/search', array( + $search_response = $this->auth->make_api_request('/Contacts/search', 'GET', array( 'criteria' => "(Email:equals:{$contact_data['Email']})" )); if (!empty($search_response['data'])) { // Update existing contact $contact_id = $search_response['data'][0]['id']; - $update_response = $this->auth->make_api_request('PUT', "/crm/v2/Contacts/{$contact_id}", array( + $update_response = $this->auth->make_api_request("/Contacts/{$contact_id}", 'PUT', array( 'data' => array($contact_data) )); } else { // Create new contact - $create_response = $this->auth->make_api_request('POST', '/crm/v2/Contacts', array( + $create_response = $this->auth->make_api_request('/Contacts', 'POST', array( 'data' => array($contact_data) )); @@ -239,31 +344,65 @@ class HVAC_Zoho_Sync { /** * Sync Event Tickets orders to Zoho Invoices * + * @param int $offset Starting offset for pagination + * @param int $limit Number of records per batch + * @param int|null $since_timestamp Optional timestamp to filter modified records * @return array Sync results */ - public function sync_purchases() { + public function sync_purchases($offset = 0, $limit = 50, $since_timestamp = null) { $results = array( 'total' => 0, 'synced' => 0, 'failed' => 0, 'errors' => array(), - 'staging_mode' => $this->is_staging + 'staging_mode' => $this->is_staging, + 'has_more' => false, + 'next_offset' => 0, + 'batch_offset' => $offset, + 'incremental_sync' => !is_null($since_timestamp) ); - // Get Tickets Commerce orders (tec_tc_order post type) - $orders = get_posts(array( + // Build query args + $query_args = array( 'post_type' => 'tec_tc_order', - 'posts_per_page' => -1, 'post_status' => 'tec-tc-completed', - )); + 'posts_per_page' => -1, + 'fields' => 'ids' + ); + + // Add date filter for incremental sync + if ($since_timestamp) { + $query_args['date_query'] = array( + array( + 'column' => 'post_modified', + 'after' => date('Y-m-d H:i:s', $since_timestamp), + 'inclusive' => true, + ) + ); + } - $results['total'] = count($orders); + // Get total count first + $count_query = new WP_Query($query_args); + $total_count = $count_query->found_posts; + $results['total'] = $total_count; - if ($results['total'] === 0) { + if ($total_count === 0) { $results['message'] = 'No completed ticket orders found.'; return $results; } + // Get paginated orders + $paginated_args = array_merge($query_args, array( + 'posts_per_page' => $limit, + 'offset' => $offset, + 'fields' => 'all', + )); + $orders = get_posts($paginated_args); + + // Calculate pagination + $results['has_more'] = ($offset + count($orders)) < $total_count; + $results['next_offset'] = $offset + count($orders); + // If staging mode, simulate the sync if ($this->is_staging) { $results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.'; @@ -336,9 +475,12 @@ class HVAC_Zoho_Sync { /** * Sync ticket attendees to Zoho Contacts + Campaign Members * + * @param int $offset Starting offset for pagination + * @param int $limit Number of records per batch + * @param int|null $since_timestamp Optional timestamp to filter modified records * @return array Sync results */ - public function sync_attendees() { + public function sync_attendees($offset = 0, $limit = 50, $since_timestamp = null) { $results = array( 'total' => 0, 'synced' => 0, @@ -346,27 +488,82 @@ class HVAC_Zoho_Sync { 'errors' => array(), 'staging_mode' => $this->is_staging, 'contacts_created' => 0, - 'campaign_members_created' => 0 + 'campaign_members_created' => 0, + 'responses' => array(), + 'debug_info' => HVAC_Zoho_CRM_Auth::get_debug_mode_info(), + 'version' => 'BATCH_V1', + 'has_more' => false, + 'next_offset' => 0, + 'batch_offset' => $offset, + 'incremental_sync' => !is_null($since_timestamp) ); - // Get all ticket attendees (Tickets Commerce) - $attendees = get_posts(array( + // Build base query args + $base_tc_args = array( 'post_type' => 'tec_tc_attendee', - 'posts_per_page' => -1, 'post_status' => 'any', - )); - - // Also get PayPal attendees if any - $tpp_attendees = get_posts(array( + 'posts_per_page' => -1, + 'fields' => 'ids' + ); + $base_tpp_args = array( 'post_type' => 'tribe_tpp_attendees', - 'posts_per_page' => -1, 'post_status' => 'any', + 'posts_per_page' => -1, + 'fields' => 'ids' + ); + + // Add date filter for incremental sync + if ($since_timestamp) { + $date_query = array( + array( + 'column' => 'post_date', + 'after' => date('Y-m-d H:i:s', $since_timestamp), + 'inclusive' => true, + ) + ); + $base_tc_args['date_query'] = $date_query; + $base_tpp_args['date_query'] = $date_query; + } + + // Get total count for all attendee types + $tc_count_query = new WP_Query($base_tc_args); + $tpp_count_query = new WP_Query($base_tpp_args); + $total_count = $tc_count_query->found_posts + $tpp_count_query->found_posts; + $results['total'] = $total_count; + + if ($total_count === 0) { + $results['message'] = 'No ticket attendees found.'; + return $results; + } + + // Get paginated attendees - first from tec_tc_attendee + $tc_paginated_args = array_merge($base_tc_args, array( + 'posts_per_page' => $limit, + 'offset' => $offset, + 'fields' => 'all', )); + $tc_attendees = get_posts($tc_paginated_args); - $all_attendees = array_merge($attendees, $tpp_attendees); - $results['total'] = count($all_attendees); + // If we need more to fill the batch, get from PayPal attendees + $remaining = max(0, $limit - count($tc_attendees)); + $tpp_offset = max(0, $offset - $tc_count_query->found_posts); + $tpp_attendees = array(); + if ($remaining > 0 && $tpp_offset >= 0) { + $tpp_paginated_args = array_merge($base_tpp_args, array( + 'posts_per_page' => $remaining, + 'offset' => $tpp_offset, + 'fields' => 'all', + )); + $tpp_attendees = get_posts($tpp_paginated_args); + } - if ($results['total'] === 0) { + $all_attendees = array_merge($tc_attendees, $tpp_attendees); + + // Calculate pagination + $results['has_more'] = ($offset + count($all_attendees)) < $total_count; + $results['next_offset'] = $offset + count($all_attendees); + + if (count($all_attendees) === 0) { $results['message'] = 'No ticket attendees found.'; return $results; } @@ -391,40 +588,104 @@ class HVAC_Zoho_Sync { return $results; } - foreach ($all_attendees as $attendee) { - try { - $attendee_data = $this->prepare_attendee_data($attendee); + $results['version'] = 'PRODUCTION_BATCH_V1'; - if (empty($attendee_data['email'])) { - $results['failed']++; - $results['errors'][] = sprintf('Attendee %s: No email address', $attendee->ID); - continue; - } + // Wrap entire loop in try-catch for safety (catches PHP 7+ errors) + try { + foreach ($all_attendees as $attendee) { + + try { + $attendee_data = $this->prepare_attendee_data($attendee); - // Step 1: Create/Update Contact - $contact_id = $this->ensure_contact_exists($attendee_data); - if ($contact_id) { - $results['contacts_created']++; - } - - // Step 2: Create Campaign Member (link Contact to Campaign) - if ($contact_id && !empty($attendee_data['event_id'])) { - $campaign_id = get_post_meta($attendee_data['event_id'], '_zoho_campaign_id', true); - if ($campaign_id) { - $this->create_campaign_member($contact_id, $campaign_id, 'Attended'); - $results['campaign_members_created']++; + // Check for any usable email (fallback to mq_email if email is empty) + $has_email = !empty($attendee_data['email']) || !empty($attendee_data['mq_email']); + if (!$has_email) { + $results['failed']++; + $results['errors'][] = sprintf('Attendee %s: No email address', $attendee->ID); + $processed++; + continue; } + + // Step 1: Create/Update Contact + $contact_id = $this->ensure_contact_exists($attendee_data); + if ($contact_id) { + $results['contacts_created']++; + } + + if (count($results['responses']) < 5) { + $cid_debug = $contact_id ? $contact_id : 'NULL'; + $results['responses'][] = array('type' => 'debug', 'msg' => "Debug: Attendee {$attendee->ID} found Contact ID: " . $cid_debug); + } + + // Step 2: Create Campaign Member (link Contact to Campaign) + if ($contact_id && !empty($attendee_data['event_id'])) { + $campaign_id = get_post_meta($attendee_data['event_id'], '_zoho_campaign_id', true); + + // Debug: Log event_id and campaign_id for troubleshooting + if (count($results['responses']) < 10) { + $results['responses'][] = array( + 'type' => 'campaign_lookup', + 'attendee_id' => $attendee->ID, + 'event_id' => $attendee_data['event_id'], + 'campaign_id' => $campaign_id ?: 'NOT_SET' + ); + } + + if ($campaign_id) { + $assoc_response = $this->create_campaign_member($contact_id, $campaign_id, 'Attended'); + + // ALWAYS capture the first link attempt for debugging + if (!isset($results['first_link_attempt'])) { + $results['first_link_attempt'] = array( + 'attendee_id' => $attendee->ID, + 'contact_id' => $contact_id, + 'campaign_id' => $campaign_id, + 'response' => $assoc_response + ); + } + + // Debug: Add responses (increased limit to 20) + if (count($results['responses']) < 20) { + $results['responses'][] = array('type' => 'link_attempt', 'id' => $attendee->ID, 'response' => $assoc_response); + } + + if (isset($assoc_response['status']) && $assoc_response['status'] === 'success') { + $results['campaign_members_created']++; + } elseif (isset($assoc_response['data'][0]['status']) && $assoc_response['data'][0]['status'] === 'success') { + $results['campaign_members_created']++; + } else { + $results['errors'][] = sprintf('Attendee %s: Failed to link to campaign. Response: %s', $attendee->ID, json_encode($assoc_response)); + if (isset($assoc_response['data'])) { + $results['responses'][] = array('type' => 'link_error', 'id' => $attendee->ID, 'response' => $assoc_response); + } + } + } else { + $results['errors'][] = sprintf('Attendee %s: Event %s has no Zoho Campaign ID', $attendee->ID, $attendee_data['event_id']); + } + } elseif (!$contact_id) { + $error_detail = $this->last_contact_error ?: 'Unknown error'; + $results['errors'][] = sprintf('Attendee %s: No contact_id created. %s', $attendee->ID, $error_detail); + } elseif (empty($attendee_data['event_id'])) { + // Debug: Log when event_id is missing + if (count($results['responses']) < 10) { + $results['responses'][] = array('type' => 'missing_event_id', 'attendee_id' => $attendee->ID); + } + } + + $results['synced']++; + + // Update attendee meta with Zoho Contact ID + update_post_meta($attendee->ID, '_zoho_contact_id', $contact_id); + + } catch (\Throwable $e) { + // Catch both Exception and Error (PHP 7+) + $results['failed']++; + $results['errors'][] = sprintf('Attendee %s: [%s] %s', $attendee->ID, get_class($e), $e->getMessage()); } - - $results['synced']++; - - // Update attendee meta with Zoho Contact ID - update_post_meta($attendee->ID, '_zoho_contact_id', $contact_id); - - } catch (Exception $e) { - $results['failed']++; - $results['errors'][] = sprintf('Attendee %s: %s', $attendee->ID, $e->getMessage()); } + } catch (\Throwable $e) { + // Outer catch for any loop-level errors + $results['errors'][] = 'Loop error: [' . get_class($e) . '] ' . $e->getMessage(); } return $results; @@ -433,9 +694,12 @@ class HVAC_Zoho_Sync { /** * Sync RSVPs to Zoho Leads + Campaign Members * + * @param int $offset Starting offset for pagination + * @param int $limit Number of records per batch + * @param int|null $since_timestamp Optional timestamp to filter modified records * @return array Sync results */ - public function sync_rsvps() { + public function sync_rsvps($offset = 0, $limit = 50, $since_timestamp = null) { $results = array( 'total' => 0, 'synced' => 0, @@ -443,27 +707,59 @@ class HVAC_Zoho_Sync { 'errors' => array(), 'staging_mode' => $this->is_staging, 'leads_created' => 0, - 'campaign_members_created' => 0 + 'campaign_members_created' => 0, + 'debug_info' => HVAC_Zoho_CRM_Auth::get_debug_mode_info(), + 'has_more' => false, + 'next_offset' => 0, + 'batch_offset' => $offset, + 'incremental_sync' => !is_null($since_timestamp) ); - // Get RSVP attendees - $rsvps = get_posts(array( + // Build query args + $query_args = array( 'post_type' => 'tribe_rsvp_attendees', - 'posts_per_page' => -1, 'post_status' => 'any', - )); + 'posts_per_page' => -1, + 'fields' => 'ids' + ); + + // Add date filter for incremental sync + if ($since_timestamp) { + $query_args['date_query'] = array( + array( + 'column' => 'post_date', + 'after' => date('Y-m-d H:i:s', $since_timestamp), + 'inclusive' => true, + ) + ); + } - $results['total'] = count($rsvps); + // Get total count first + $count_query = new WP_Query($query_args); + $total_count = $count_query->found_posts; + $results['total'] = $total_count; - if ($results['total'] === 0) { + if ($total_count === 0) { $results['message'] = 'No RSVP attendees found.'; return $results; } + // Get paginated RSVPs + $paginated_args = array_merge($query_args, array( + 'posts_per_page' => $limit, + 'offset' => $offset, + 'fields' => 'all', + )); + $rsvps = get_posts($paginated_args); + + // Calculate pagination + $results['has_more'] = ($offset + count($rsvps)) < $total_count; + $results['next_offset'] = $offset + count($rsvps); + // If staging mode, simulate the sync if ($this->is_staging) { $results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.'; - $results['synced'] = $results['total']; + $results['synced'] = count($rsvps); $results['test_data'] = array(); foreach ($rsvps as $rsvp) { @@ -500,8 +796,19 @@ class HVAC_Zoho_Sync { if ($lead_id && !empty($rsvp_data['event_id'])) { $campaign_id = get_post_meta($rsvp_data['event_id'], '_zoho_campaign_id', true); if ($campaign_id) { - $this->create_campaign_member($lead_id, $campaign_id, 'Responded', 'Leads'); - $results['campaign_members_created']++; + $assoc_response = $this->create_campaign_member($lead_id, $campaign_id, 'Responded', 'Leads'); + if (isset($assoc_response['status']) && $assoc_response['status'] === 'success') { + $results['campaign_members_created']++; + } elseif (isset($assoc_response['data'][0]['status']) && $assoc_response['data'][0]['status'] === 'success') { + $results['campaign_members_created']++; + } else { + $results['errors'][] = sprintf('RSVP %s: Failed to link to campaign. Response: %s', $rsvp->ID, json_encode($assoc_response)); + if (isset($assoc_response['data'])) { + $results['responses'][] = array('type' => 'link_error', 'id' => $rsvp->ID, 'response' => $assoc_response); + } + } + } else { + $results['errors'][] = sprintf('RSVP %s: Event %s has no Zoho Campaign ID', $rsvp->ID, $rsvp_data['event_id']); } } @@ -520,32 +827,70 @@ class HVAC_Zoho_Sync { } /** - * Ensure a Contact exists in Zoho (create or get existing) + * Ensure a Contact exists in Zoho (create or get existing, update fields) * * @param array $data Attendee data * @return string|null Zoho Contact ID */ private function ensure_contact_exists($data) { - // Search for existing contact by email - $search_response = $this->auth->make_api_request( - '/Contacts/search?criteria=(Email:equals:' . urlencode($data['email']) . ')', - 'GET' - ); - - if (!empty($search_response['data'])) { - return $search_response['data'][0]['id']; + // Determine primary email for lookup + // Prefer measureQuick email, fallback to attendee email if mq_email is empty + // If both are empty, we can't create a contact + $attendee_email = !empty($data['email']) ? $data['email'] : ''; + $mq_email = !empty($data['mq_email']) ? $data['mq_email'] : ''; + + // Use mq_email as primary if available, otherwise use attendee_email + $primary_email = !empty($mq_email) ? $mq_email : $attendee_email; + + // Set secondary email (only if different from primary and not empty) + $other_email = ''; + if (!empty($mq_email) && !empty($attendee_email) && $mq_email !== $attendee_email) { + $other_email = $attendee_email; } - - // Create new contact + + // Build contact data for create or update $contact_data = array( 'First_Name' => $data['first_name'] ?: 'Unknown', 'Last_Name' => $data['last_name'] ?: 'Attendee', - 'Email' => $data['email'], + 'Email' => $primary_email, 'Lead_Source' => 'Event Tickets', 'Contact_Type' => 'Attendee', 'WordPress_Attendee_ID' => $data['attendee_id'], ); + + // Add Other Email if we have both emails + if (!empty($other_email)) { + $contact_data['Secondary_Email'] = $other_email; + } + + // Add Mobile phone if available + if (!empty($data['phone'])) { + $contact_data['Mobile'] = $data['phone']; + } + + // Add Primary Role if available + if (!empty($data['company_role'])) { + $contact_data['Primary_Role'] = $data['company_role']; + } + + // Search for existing contact by primary email + $search_response = $this->auth->make_api_request( + '/Contacts/search?criteria=(Email:equals:' . urlencode($primary_email) . ')', + 'GET' + ); + if (!empty($search_response['data'])) { + $contact_id = $search_response['data'][0]['id']; + + // UPDATE existing contact with new field data + $this->auth->make_api_request("/Contacts/{$contact_id}", 'PUT', array( + 'data' => array($contact_data) + )); + + return $contact_id; + } + + // Create new contact $create_response = $this->auth->make_api_request('/Contacts', 'POST', array( 'data' => array($contact_data) )); @@ -553,6 +898,26 @@ class HVAC_Zoho_Sync { if (!empty($create_response['data'][0]['details']['id'])) { return $create_response['data'][0]['details']['id']; } + + // Handle DUPLICATE_DATA: Contact exists but with different email + // Extract existing contact ID from the error response and use it + if (!empty($create_response['data'][0]['code']) && + $create_response['data'][0]['code'] === 'DUPLICATE_DATA' && + !empty($create_response['data'][0]['details']['duplicate_record']['id'])) { + + $existing_id = $create_response['data'][0]['details']['duplicate_record']['id']; + + // Update the existing contact with new data + $this->auth->make_api_request("/Contacts/{$existing_id}", 'PUT', array( + 'data' => array($contact_data) + )); + + return $existing_id; + } + + // Store error for debugging and log it + $this->last_contact_error = "Email: {$primary_email}, Response: " . json_encode($create_response); + $this->auth->log_debug("Failed to create contact: " . $this->last_contact_error); return null; } @@ -604,13 +969,26 @@ class HVAC_Zoho_Sync { * @param string $type 'Contacts' or 'Leads' */ private function create_campaign_member($record_id, $campaign_id, $status = 'Attended', $type = 'Contacts') { - $endpoint = "/Campaigns/{$campaign_id}/{$type}/{$record_id}"; + // v6 API: Associate Contact/Lead with Campaign via related list + // PUT /{Contacts|Leads}/{record_id}/Campaigns + // The Contact/Lead is the parent, Campaigns is the related list + // Campaign ID goes in the request body, not the URL + + $endpoint = "/{$type}/{$record_id}/Campaigns"; + + // Debug: Log the association attempt + $this->auth->log_debug("Attempting to associate {$type} {$record_id} with Campaign {$campaign_id} via {$endpoint}"); - $this->auth->make_api_request($endpoint, 'PUT', array( + $response = $this->auth->make_api_request($endpoint, 'PUT', array( 'data' => array( - array('Member_Status' => $status) + array( + 'id' => $campaign_id, + 'Member_Status' => $status + ) ) )); + + return $response; } /** @@ -622,22 +1000,112 @@ class HVAC_Zoho_Sync { private function prepare_campaign_data($event) { $trainer_id = get_post_meta($event->ID, '_trainer_id', true); $trainer = get_user_by('id', $trainer_id); - $venue = tribe_get_venue($event->ID); + $venue_id = tribe_get_venue_id($event->ID); - return array( - 'Campaign_Name' => get_the_title($event->ID), + // Get venue details + $venue_name = ''; + $venue_city = ''; + $venue_state = ''; + $venue_details = ''; + + if ($venue_id) { + $venue_name = html_entity_decode(get_the_title($venue_id), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $venue_city = tribe_get_city($event->ID); + $venue_state = tribe_get_state($event->ID); + $venue_address = tribe_get_address($event->ID); + + // Build venue details string + $venue_parts = array_filter(array($venue_name, $venue_address, $venue_city, $venue_state)); + $venue_details = implode(', ', $venue_parts); + } + + // Sanitize description: strip tags and limit length, then decode entities + $description = wp_strip_all_tags(get_the_content(null, false, $event)); + $description = html_entity_decode($description, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + if (strlen($description) > 30000) { + $description = substr($description, 0, 30000) . '...'; + } + + $campaign_name = html_entity_decode(get_the_title($event->ID), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $trainer_name = $trainer ? html_entity_decode($trainer->display_name, ENT_QUOTES | ENT_HTML5, 'UTF-8') : ''; + + // Get trainer's Zoho Contact ID (if they've been synced) + $instructor_zoho_id = ''; + if ($trainer) { + $instructor_zoho_id = get_user_meta($trainer->ID, '_zoho_contact_id', true); + } + + // Calculate ticket counts + $total_capacity = intval(get_post_meta($event->ID, '_stock', true)); + $tickets_sold = $this->get_attendee_count($event->ID); + $tickets_available = max(0, $total_capacity - $tickets_sold); + + // Build campaign data array + $data = array( + 'Campaign_Name' => substr($campaign_name, 0, 200), 'Start_Date' => tribe_get_start_date($event->ID, false, 'Y-m-d'), 'End_Date' => tribe_get_end_date($event->ID, false, 'Y-m-d'), 'Status' => (tribe_get_end_date($event->ID, false, 'U') < time()) ? 'Completed' : 'Active', - 'Description' => get_the_content(null, false, $event), + 'Description' => $description, 'Type' => 'Training Event', 'Expected_Revenue' => floatval(get_post_meta($event->ID, '_price', true)), - 'Total_Capacity' => intval(get_post_meta($event->ID, '_stock', true)), - 'Venue' => $venue ? get_the_title($venue) : '', - 'Trainer_Name' => $trainer ? $trainer->display_name : '', - 'Trainer_Email' => $trainer ? $trainer->user_email : '', - 'WordPress_Event_ID' => $event->ID + 'Total_Capacity' => $total_capacity, + 'Tickets_Sold' => $tickets_sold, + 'Tickets_Available' => $tickets_available, + 'Event_City' => substr($venue_city, 0, 100), + 'Event_State' => substr($venue_state, 0, 100), + 'Event_URL' => get_permalink($event->ID), + 'Venue_Details' => substr($venue_details, 0, 250), + 'Instructor_Email' => $trainer ? $trainer->user_email : '', ); + + // Add Instructor lookup only if we have a valid Zoho Contact ID + if (!empty($instructor_zoho_id)) { + $data['Instructor'] = $instructor_zoho_id; + } + + return $data; + } + + /** + * Get count of attendees for an event + * + * @param int $event_id Event post ID + * @return int Attendee count + */ + private function get_attendee_count($event_id) { + // Check Tickets Commerce attendees + $tc_attendees = get_posts(array( + 'post_type' => 'tec_tc_attendee', + 'posts_per_page' => -1, + 'post_status' => 'any', + 'meta_query' => array( + array( + 'key' => '_tec_tickets_commerce_event', + 'value' => $event_id, + 'compare' => '=' + ) + ), + 'fields' => 'ids' + )); + + // Also check PayPal attendees + $tpp_attendees = get_posts(array( + 'post_type' => 'tribe_tpp_attendees', + 'posts_per_page' => -1, + 'post_status' => 'any', + 'meta_query' => array( + array( + 'key' => '_tribe_tpp_event', + 'value' => $event_id, + 'compare' => '=' + ) + ), + 'fields' => 'ids' + )); + + return count($tc_attendees) + count($tpp_attendees); } /** @@ -657,12 +1125,12 @@ class HVAC_Zoho_Sync { } return array( - 'First_Name' => get_user_meta($user->ID, 'first_name', true), - 'Last_Name' => get_user_meta($user->ID, 'last_name', true), + 'First_Name' => html_entity_decode(get_user_meta($user->ID, 'first_name', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'), + 'Last_Name' => html_entity_decode(get_user_meta($user->ID, 'last_name', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Email' => $user->user_email, 'Phone' => get_user_meta($user->ID, 'phone_number', true), - 'Title' => get_user_meta($user->ID, 'hvac_professional_title', true), - 'Company' => get_user_meta($user->ID, 'hvac_company_name', true), + 'Title' => html_entity_decode(get_user_meta($user->ID, 'hvac_professional_title', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'), + 'Company' => html_entity_decode(get_user_meta($user->ID, 'hvac_company_name', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Lead_Source' => 'HVAC Community Events', 'Contact_Type' => $role, 'WordPress_User_ID' => $user->ID, @@ -746,6 +1214,21 @@ class HVAC_Zoho_Sync { $full_name = get_post_meta($attendee->ID, '_tec_tickets_commerce_full_name', true); $email = get_post_meta($attendee->ID, '_tec_tickets_commerce_email', true); $checkin = get_post_meta($attendee->ID, '_tec_tickets_commerce_checked_in', true); + + // Custom attendee fields stored in _tec_tickets_commerce_attendee_fields (serialized array) + // Field names use hyphens: attendee-cell-phone, company-role, measurequick-email + $attendee_fields = get_post_meta($attendee->ID, '_tec_tickets_commerce_attendee_fields', true); + + $phone = ''; + $company_role = ''; + $mq_email = ''; + + if (is_array($attendee_fields)) { + $phone = isset($attendee_fields['attendee-cell-phone']) ? $attendee_fields['attendee-cell-phone'] : ''; + $company_role = isset($attendee_fields['company-role']) ? $attendee_fields['company-role'] : ''; + $mq_email = isset($attendee_fields['measurequick-email']) ? $attendee_fields['measurequick-email'] : ''; + } + } else { // PayPal or other attendee types (tribe_tpp_attendees) $event_id = get_post_meta($attendee->ID, '_tribe_tpp_event', true); @@ -754,6 +1237,12 @@ class HVAC_Zoho_Sync { $full_name = get_post_meta($attendee->ID, '_tribe_tickets_full_name', true); $email = get_post_meta($attendee->ID, '_tribe_tickets_email', true); $checkin = get_post_meta($attendee->ID, '_tribe_tpp_checkin', true); + + // Legacy attendee meta for custom fields (hyphenated keys) + $attendee_meta = get_post_meta($attendee->ID, '_tribe_tickets_meta', true); + $phone = is_array($attendee_meta) && isset($attendee_meta['attendee-cell-phone']) ? $attendee_meta['attendee-cell-phone'] : ''; + $company_role = is_array($attendee_meta) && isset($attendee_meta['company-role']) ? $attendee_meta['company-role'] : ''; + $mq_email = is_array($attendee_meta) && isset($attendee_meta['measurequick-email']) ? $attendee_meta['measurequick-email'] : ''; } // Parse full name into first/last @@ -776,6 +1265,9 @@ class HVAC_Zoho_Sync { 'first_name' => $first_name, 'last_name' => $last_name, 'email' => $email, + 'phone' => $phone, + 'company_role' => $company_role, + 'mq_email' => $mq_email, 'checked_in' => !empty($checkin), 'zoho_contact' => array( 'First_Name' => $first_name ?: 'Unknown',