fix(zoho): Fix silent sync failures with API response validation and hash reset
Zoho CRM sync appeared connected but silently failed to write data due to unvalidated API responses. Sync methods now validate Zoho responses before updating hashes, ensuring failed records re-sync on next run. Also fixes staging detection to use wp_parse_url hostname parsing instead of fragile strpos matching, adds admin UI for resetting sync hashes, and bumps HVAC_PLUGIN_VERSION to 2.2.11 to bust browser cache for updated JS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4b53d3eab6
commit
03b9bce52d
6 changed files with 416 additions and 106 deletions
81
Status.md
81
Status.md
|
|
@ -1,8 +1,8 @@
|
||||||
# HVAC Community Events - Project Status
|
# HVAC Community Events - Project Status
|
||||||
|
|
||||||
**Last Updated:** February 6, 2026
|
**Last Updated:** February 6, 2026
|
||||||
**Current Session:** Near Me Button Mobile Fix
|
**Current Session:** Zoho CRM Sync Fix
|
||||||
**Version:** 2.2.6 (Deployed to Staging)
|
**Version:** 2.2.6 (Deployed to Production)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -18,9 +18,84 @@
|
||||||
- Contact forms (trainer, venue)
|
- Contact forms (trainer, venue)
|
||||||
- Any other public-facing forms
|
- Any other public-facing forms
|
||||||
|
|
||||||
|
### Post-Deploy Action Required
|
||||||
|
On production, go to **wp-admin > HVAC Community Events > Zoho CRM Sync**, click **"Force Full Re-sync (Reset Hashes)"**, then click each sync button or wait for the hourly scheduled sync. This clears poisoned hashes from prior failed syncs so all records re-sync with the new validated code.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 CURRENT SESSION - NEAR ME BUTTON MOBILE FIX (Feb 6, 2026)
|
## 🎯 CURRENT SESSION - ZOHO CRM SYNC FIX (Feb 6, 2026)
|
||||||
|
|
||||||
|
### Status: ✅ **COMPLETE - Deployed to Production**
|
||||||
|
|
||||||
|
**Objective:** Fix Zoho CRM integration that appeared connected but was silently failing to sync data (events, attendees, ticket sales not appearing in Zoho CRM).
|
||||||
|
|
||||||
|
### Root Cause Analysis (4-model consensus: GPT-5, Gemini 3, Zen Code Review, Zen Debug)
|
||||||
|
|
||||||
|
The sync pipeline had a "silent failure" architecture:
|
||||||
|
1. **Connection test only uses GET** - bypasses staging write block, giving false confidence
|
||||||
|
2. **Staging mode returned fake `'status' => 'success'`** for blocked writes - appeared successful
|
||||||
|
3. **Sync methods unconditionally updated `_zoho_sync_hash`** after any "sync" - poisoned hashes
|
||||||
|
4. **Subsequent syncs compared hashes, found matches, skipped all records** - permanent data loss
|
||||||
|
5. **No API response validation** - even real Zoho errors were silently ignored
|
||||||
|
|
||||||
|
### Fixes Implemented
|
||||||
|
|
||||||
|
1. **API Response Validation** (`includes/zoho/class-zoho-sync.php`)
|
||||||
|
- Added `validate_api_response()` helper method
|
||||||
|
- Checks for WP_Error, staging mode blocks, HTTP errors, Zoho error codes
|
||||||
|
- Confirms success with ID extraction before treating a sync as successful
|
||||||
|
|
||||||
|
2. **Hash-Only-On-Success** (`includes/zoho/class-zoho-sync.php`)
|
||||||
|
- All 5 sync methods (events, users, attendees, rsvps, purchases) rewritten
|
||||||
|
- `_zoho_sync_hash` only updates when Zoho API confirms the write succeeded
|
||||||
|
- Failed syncs increment `$results['failed']` with error details
|
||||||
|
- Changed `catch (Exception)` to `catch (\Throwable)` for comprehensive error handling
|
||||||
|
|
||||||
|
3. **Staging Mode Detection Fix** (`includes/zoho/class-zoho-crm-auth.php`)
|
||||||
|
- Replaced fragile `strpos()` substring matching with `wp_parse_url()` hostname comparison
|
||||||
|
- Production (`upskillhvac.com` / `www.upskillhvac.com`) explicitly whitelisted
|
||||||
|
- All other hostnames default to staging mode
|
||||||
|
- `HVAC_ZOHO_PRODUCTION_MODE` / `HVAC_ZOHO_STAGING_MODE` constants can override
|
||||||
|
- Staging fake responses now return `'skipped_staging'` instead of misleading `'success'`
|
||||||
|
|
||||||
|
4. **Admin UI: Hash Reset & Staging Warning** (`includes/admin/class-zoho-admin.php`, `assets/js/zoho-admin.js`)
|
||||||
|
- Added `reset_sync_hashes()` AJAX handler - clears all `_zoho_sync_hash` from postmeta and usermeta
|
||||||
|
- Added "Force Full Re-sync (Reset Hashes)" button with confirmation dialog
|
||||||
|
- Connection test now surfaces staging warning when staging mode is active
|
||||||
|
|
||||||
|
5. **Staging Environment Fix** (staging `wp-config.php`)
|
||||||
|
- Removed `HVAC_ZOHO_PRODUCTION_MODE` constant from staging wp-config.php
|
||||||
|
- Staging now correctly blocks all Zoho write operations via hostname detection
|
||||||
|
- GET requests (reads) still pass through for testing
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `includes/zoho/class-zoho-sync.php` | Added `validate_api_response()`, rewrote all 5 sync methods for validated hashing |
|
||||||
|
| `includes/zoho/class-zoho-crm-auth.php` | Rewrote `is_staging_mode()` with hostname parsing, changed fake response status |
|
||||||
|
| `includes/admin/class-zoho-admin.php` | Added `reset_sync_hashes()` handler, reset button HTML, staging warning in connection test |
|
||||||
|
| `assets/js/zoho-admin.js` | Added reset hashes button handler, staging warning display in connection test |
|
||||||
|
|
||||||
|
### Staging Verification
|
||||||
|
|
||||||
|
| Test | Result |
|
||||||
|
|------|--------|
|
||||||
|
| Pre-deployment checks | All passed |
|
||||||
|
| Connection Test button | "Connection successful!" |
|
||||||
|
| Staging mode detection | Correctly reports hostname-based staging |
|
||||||
|
| Force Full Re-sync button | Cleared 3 post hashes + 66 user hashes |
|
||||||
|
| Events Sync | 41/41 synced, 0 failed |
|
||||||
|
| Staging write-block (after wp-config fix) | GET: PASS (95 modules), POST: PASS (blocked) |
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
- Deployed successfully to https://upskillhvac.com/
|
||||||
|
- All fix code verified present on production server
|
||||||
|
- Production correctly detected as non-staging (`upskillhvac.com` hostname match)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 PREVIOUS SESSION - NEAR ME BUTTON MOBILE FIX (Feb 6, 2026)
|
||||||
|
|
||||||
### Status: ✅ **COMPLETE - Deployed to Staging**
|
### Status: ✅ **COMPLETE - Deployed to Staging**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,13 @@ jQuery(document).ready(function ($) {
|
||||||
successHtml += '<p>Refresh Token: ❌ Missing (OAuth required)</p>';
|
successHtml += '<p>Refresh Token: ❌ Missing (OAuth required)</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show staging warning if present
|
||||||
|
if (response.data.staging_warning) {
|
||||||
|
successHtml += '<div style="margin-top: 10px; padding: 8px 12px; background: #fff8e5; border-left: 3px solid #ffb900;">';
|
||||||
|
successHtml += '<p style="margin: 0; color: #826200;"><strong>Warning:</strong> ' + response.data.staging_warning + '</p>';
|
||||||
|
successHtml += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// Show debug info if available
|
// Show debug info if available
|
||||||
if (response.data.debug) {
|
if (response.data.debug) {
|
||||||
successHtml += '<details style="margin-top: 10px;">';
|
successHtml += '<details style="margin-top: 10px;">';
|
||||||
|
|
@ -622,6 +629,47 @@ jQuery(document).ready(function ($) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// Reset Sync Hashes Handler
|
||||||
|
// =====================================================
|
||||||
|
$('#reset-sync-hashes').on('click', function () {
|
||||||
|
if (!confirm('This will clear all sync hashes and force every record to re-sync on the next run. Continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $button = $(this);
|
||||||
|
var $status = $('#reset-hashes-status');
|
||||||
|
|
||||||
|
$button.prop('disabled', true).text('Resetting...');
|
||||||
|
$status.text('');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: hvacZoho.ajaxUrl,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'hvac_zoho_reset_sync_hashes',
|
||||||
|
nonce: hvacZoho.nonce
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
$status.html(
|
||||||
|
'<span style="color: #46b450;">' +
|
||||||
|
response.data.posts_cleared + ' post hashes and ' +
|
||||||
|
response.data.users_cleared + ' user hashes cleared.</span>'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$status.html('<span style="color: #dc3232;">Error: ' + response.data.message + '</span>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$status.html('<span style="color: #dc3232;">Network error - please try again</span>');
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
$button.prop('disabled', false).text('Force Full Re-sync (Reset Hashes)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Diagnostic Test Handler
|
// Diagnostic Test Handler
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ class HVAC_Zoho_Admin {
|
||||||
add_action('wp_ajax_hvac_zoho_run_scheduled_sync', array($this, 'run_scheduled_sync_now'));
|
add_action('wp_ajax_hvac_zoho_run_scheduled_sync', array($this, 'run_scheduled_sync_now'));
|
||||||
// Add simple test handler
|
// Add simple test handler
|
||||||
add_action('wp_ajax_hvac_zoho_simple_test', array($this, 'simple_test'));
|
add_action('wp_ajax_hvac_zoho_simple_test', array($this, 'simple_test'));
|
||||||
|
// Add hash reset handler
|
||||||
|
add_action('wp_ajax_hvac_zoho_reset_sync_hashes', array($this, 'reset_sync_hashes'));
|
||||||
// Add OAuth callback handler - only use one method to prevent duplicates
|
// Add OAuth callback handler - only use one method to prevent duplicates
|
||||||
add_action('init', array($this, 'add_oauth_rewrite_rule'), 5);
|
add_action('init', array($this, 'add_oauth_rewrite_rule'), 5);
|
||||||
add_filter('query_vars', array($this, 'add_oauth_query_vars'), 10, 1);
|
add_filter('query_vars', array($this, 'add_oauth_query_vars'), 10, 1);
|
||||||
|
|
@ -297,6 +299,13 @@ class HVAC_Zoho_Admin {
|
||||||
<div class="hvac-zoho-sync">
|
<div class="hvac-zoho-sync">
|
||||||
<h2>Data Sync</h2>
|
<h2>Data Sync</h2>
|
||||||
|
|
||||||
|
<div class="sync-maintenance" style="margin-bottom: 20px; padding: 12px 15px; background: #fff8e5; border-left: 4px solid #ffb900;">
|
||||||
|
<p style="margin: 0 0 8px 0;"><strong>Sync Maintenance</strong></p>
|
||||||
|
<p style="margin: 0 0 8px 0; font-size: 13px;">If records aren't syncing (e.g. after a failed sync or configuration change), reset sync hashes to force all records to re-sync on the next run.</p>
|
||||||
|
<button class="button" id="reset-sync-hashes">Force Full Re-sync (Reset Hashes)</button>
|
||||||
|
<span id="reset-hashes-status" style="margin-left: 10px;"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sync-section">
|
<div class="sync-section">
|
||||||
<h3>Events → Campaigns</h3>
|
<h3>Events → Campaigns</h3>
|
||||||
<p>Sync events from The Events Calendar to Zoho CRM Campaigns</p>
|
<p>Sync events from The Events Calendar to Zoho CRM Campaigns</p>
|
||||||
|
|
@ -897,20 +906,28 @@ class HVAC_Zoho_Admin {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success!
|
// Success!
|
||||||
// Success!
|
$mode_info = HVAC_Zoho_CRM_Auth::get_debug_mode_info();
|
||||||
wp_send_json_success(array(
|
$response_data = array(
|
||||||
'message' => 'Connection successful!',
|
'message' => 'Connection successful!',
|
||||||
'modules' => isset($response['modules']) ? count($response['modules']) . ' modules available' : 'API connected',
|
'modules' => isset($response['modules']) ? count($response['modules']) . ' modules available' : 'API connected',
|
||||||
'client_id' => substr($client_id, 0, 10) . '...',
|
'client_id' => substr($client_id, 0, 10) . '...',
|
||||||
'client_secret_exists' => true,
|
'client_secret_exists' => true,
|
||||||
'refresh_token_exists' => true,
|
'refresh_token_exists' => true,
|
||||||
|
'is_staging' => $mode_info['is_staging'],
|
||||||
|
'mode_info' => $mode_info,
|
||||||
'credentials_status' => array(
|
'credentials_status' => array(
|
||||||
'client_id' => substr($client_id, 0, 10) . '...',
|
'client_id' => substr($client_id, 0, 10) . '...',
|
||||||
'client_secret_exists' => true,
|
'client_secret_exists' => true,
|
||||||
'refresh_token_exists' => true,
|
'refresh_token_exists' => true,
|
||||||
'api_working' => true
|
'api_working' => true
|
||||||
)
|
)
|
||||||
));
|
);
|
||||||
|
|
||||||
|
if ($mode_info['is_staging']) {
|
||||||
|
$response_data['staging_warning'] = 'WARNING: Staging mode is active. All write operations (sync) are blocked. Hostname: ' . ($mode_info['parsed_host'] ?? 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($response_data);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$error_response = array(
|
$error_response = array(
|
||||||
'message' => 'Connection test failed due to exception',
|
'message' => 'Connection test failed due to exception',
|
||||||
|
|
@ -1048,18 +1065,18 @@ class HVAC_Zoho_Admin {
|
||||||
*/
|
*/
|
||||||
public function run_scheduled_sync_now() {
|
public function run_scheduled_sync_now() {
|
||||||
check_ajax_referer('hvac_zoho_nonce', 'nonce');
|
check_ajax_referer('hvac_zoho_nonce', 'nonce');
|
||||||
|
|
||||||
if (!current_user_can('manage_options')) {
|
if (!current_user_can('manage_options')) {
|
||||||
wp_send_json_error(array('message' => 'Unauthorized access'));
|
wp_send_json_error(array('message' => 'Unauthorized access'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
|
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
|
||||||
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
|
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
|
||||||
|
|
||||||
$result = $scheduled_sync->run_now();
|
$result = $scheduled_sync->run_now();
|
||||||
|
|
||||||
wp_send_json_success(array(
|
wp_send_json_success(array(
|
||||||
'message' => 'Scheduled sync completed',
|
'message' => 'Scheduled sync completed',
|
||||||
'result' => $result
|
'result' => $result
|
||||||
|
|
@ -1072,5 +1089,42 @@ class HVAC_Zoho_Admin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all Zoho sync hashes to force a full re-sync
|
||||||
|
*/
|
||||||
|
public function reset_sync_hashes() {
|
||||||
|
check_ajax_referer('hvac_zoho_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error(array('message' => 'Unauthorized access'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Delete all _zoho_sync_hash post meta
|
||||||
|
$posts_cleared = $wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = '_zoho_sync_hash'"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete all _zoho_sync_hash user meta
|
||||||
|
$users_cleared = $wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->usermeta} WHERE meta_key = '_zoho_sync_hash'"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also clear last sync time so scheduled sync does a full run
|
||||||
|
delete_option('hvac_zoho_last_sync_time');
|
||||||
|
|
||||||
|
if (class_exists('HVAC_Logger')) {
|
||||||
|
HVAC_Logger::info("Sync hashes reset: {$posts_cleared} post hashes, {$users_cleared} user hashes cleared", 'ZohoAdmin');
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => 'Sync hashes reset successfully',
|
||||||
|
'posts_cleared' => (int) $posts_cleared,
|
||||||
|
'users_cleared' => (int) $users_cleared,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
@ -112,10 +112,10 @@ final class HVAC_Plugin {
|
||||||
*/
|
*/
|
||||||
private function defineConstants(): void {
|
private function defineConstants(): void {
|
||||||
if (!defined('HVAC_PLUGIN_VERSION')) {
|
if (!defined('HVAC_PLUGIN_VERSION')) {
|
||||||
define('HVAC_PLUGIN_VERSION', '2.0.0');
|
define('HVAC_PLUGIN_VERSION', '2.2.11');
|
||||||
}
|
}
|
||||||
if (!defined('HVAC_VERSION')) {
|
if (!defined('HVAC_VERSION')) {
|
||||||
define('HVAC_VERSION', '2.2.6');
|
define('HVAC_VERSION', '2.2.11');
|
||||||
}
|
}
|
||||||
if (!defined('HVAC_PLUGIN_FILE')) {
|
if (!defined('HVAC_PLUGIN_FILE')) {
|
||||||
define('HVAC_PLUGIN_FILE', dirname(__DIR__) . '/hvac-community-events.php');
|
define('HVAC_PLUGIN_FILE', dirname(__DIR__) . '/hvac-community-events.php');
|
||||||
|
|
@ -175,6 +175,9 @@ final class HVAC_Plugin {
|
||||||
// Core architecture includes
|
// Core architecture includes
|
||||||
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-browser-detection.php';
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-browser-detection.php';
|
||||||
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-find-trainer-assets.php';
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-find-trainer-assets.php';
|
||||||
|
|
||||||
|
// reCAPTCHA integration for contact forms
|
||||||
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-recaptcha.php';
|
||||||
|
|
||||||
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-safari-debugger.php';
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-safari-debugger.php';
|
||||||
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-shortcodes.php';
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-shortcodes.php';
|
||||||
|
|
|
||||||
|
|
@ -222,18 +222,21 @@ class HVAC_Zoho_CRM_Auth {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Parse hostname from site URL for accurate comparison
|
||||||
$site_url = get_site_url();
|
$site_url = get_site_url();
|
||||||
|
$host = wp_parse_url($site_url, PHP_URL_HOST);
|
||||||
// 3. Check for specific staging domains or keywords
|
|
||||||
if (strpos($site_url, 'staging') !== false ||
|
if (empty($host)) {
|
||||||
strpos($site_url, 'dev') !== false ||
|
return true; // Can't determine host, default to staging for safety
|
||||||
strpos($site_url, 'test') !== false ||
|
|
||||||
strpos($site_url, 'cloudwaysapps.com') !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Default check: Production only on upskillhvac.com
|
// 4. Production: upskillhvac.com or www.upskillhvac.com
|
||||||
return strpos($site_url, 'upskillhvac.com') === false;
|
if ($host === 'upskillhvac.com' || $host === 'www.upskillhvac.com') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Everything else is staging (including staging subdomains, cloudwaysapps, localhost, etc.)
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -242,8 +245,12 @@ class HVAC_Zoho_CRM_Auth {
|
||||||
* @return array Debug information
|
* @return array Debug information
|
||||||
*/
|
*/
|
||||||
public static function get_debug_mode_info() {
|
public static function get_debug_mode_info() {
|
||||||
|
$site_url = get_site_url();
|
||||||
|
$host = wp_parse_url($site_url, PHP_URL_HOST);
|
||||||
|
|
||||||
$info = array(
|
$info = array(
|
||||||
'site_url' => get_site_url(),
|
'site_url' => $site_url,
|
||||||
|
'parsed_host' => $host,
|
||||||
'is_staging' => self::is_staging_mode(),
|
'is_staging' => self::is_staging_mode(),
|
||||||
'forced_production' => defined('HVAC_ZOHO_PRODUCTION_MODE') && HVAC_ZOHO_PRODUCTION_MODE,
|
'forced_production' => defined('HVAC_ZOHO_PRODUCTION_MODE') && HVAC_ZOHO_PRODUCTION_MODE,
|
||||||
'forced_staging' => defined('HVAC_ZOHO_STAGING_MODE') && HVAC_ZOHO_STAGING_MODE,
|
'forced_staging' => defined('HVAC_ZOHO_STAGING_MODE') && HVAC_ZOHO_STAGING_MODE,
|
||||||
|
|
@ -255,20 +262,12 @@ class HVAC_Zoho_CRM_Auth {
|
||||||
$info['detection_logic'][] = 'Forced PRODUCTION via HVAC_ZOHO_PRODUCTION_MODE constant';
|
$info['detection_logic'][] = 'Forced PRODUCTION via HVAC_ZOHO_PRODUCTION_MODE constant';
|
||||||
} elseif ($info['forced_staging']) {
|
} elseif ($info['forced_staging']) {
|
||||||
$info['detection_logic'][] = 'Forced STAGING via HVAC_ZOHO_STAGING_MODE constant';
|
$info['detection_logic'][] = 'Forced STAGING via HVAC_ZOHO_STAGING_MODE constant';
|
||||||
|
} elseif (empty($host)) {
|
||||||
|
$info['detection_logic'][] = 'STAGING: Could not parse hostname from URL';
|
||||||
|
} elseif ($host === 'upskillhvac.com' || $host === 'www.upskillhvac.com') {
|
||||||
|
$info['detection_logic'][] = 'PRODUCTION: Hostname matches upskillhvac.com';
|
||||||
} else {
|
} else {
|
||||||
$site_url = $info['site_url'];
|
$info['detection_logic'][] = 'STAGING: Hostname "' . $host . '" is not upskillhvac.com';
|
||||||
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;
|
return $info;
|
||||||
|
|
@ -283,7 +282,7 @@ class HVAC_Zoho_CRM_Auth {
|
||||||
|
|
||||||
// In staging mode, only allow read operations, no writes
|
// In staging mode, only allow read operations, no writes
|
||||||
if ($is_staging && in_array($method, array('POST', 'PUT', 'DELETE', 'PATCH'))) {
|
if ($is_staging && in_array($method, array('POST', 'PUT', 'DELETE', 'PATCH'))) {
|
||||||
$this->log_debug('STAGING MODE: Simulating ' . $method . ' request to ' . $endpoint);
|
$this->log_debug('STAGING MODE: Blocked ' . $method . ' request to ' . $endpoint);
|
||||||
return array(
|
return array(
|
||||||
'data' => array(
|
'data' => array(
|
||||||
array(
|
array(
|
||||||
|
|
@ -291,8 +290,8 @@ class HVAC_Zoho_CRM_Auth {
|
||||||
'details' => array(
|
'details' => array(
|
||||||
'message' => 'Staging mode active. Write operations are disabled.'
|
'message' => 'Staging mode active. Write operations are disabled.'
|
||||||
),
|
),
|
||||||
'message' => 'This would have been a ' . $method . ' request to: ' . $endpoint,
|
'message' => 'Blocked ' . $method . ' request to: ' . $endpoint,
|
||||||
'status' => 'success'
|
'status' => 'skipped_staging'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,86 @@ class HVAC_Zoho_Sync {
|
||||||
// Use consistent logic from Auth class
|
// Use consistent logic from Auth class
|
||||||
return !HVAC_Zoho_CRM_Auth::is_staging_mode();
|
return !HVAC_Zoho_CRM_Auth::is_staging_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a Zoho API response to determine if the operation succeeded
|
||||||
|
*
|
||||||
|
* @param mixed $response Response from make_api_request()
|
||||||
|
* @return array ['success' => bool, 'id' => string|null, 'error' => string|null]
|
||||||
|
*/
|
||||||
|
private function validate_api_response($response) {
|
||||||
|
// Check for WP_Error
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'id' => null,
|
||||||
|
'error' => $response->get_error_message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for staging mode simulation
|
||||||
|
if (isset($response['data'][0]['code']) && $response['data'][0]['code'] === 'STAGING_MODE') {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'id' => null,
|
||||||
|
'error' => 'Staging mode: write operations blocked'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for HTTP-level errors
|
||||||
|
if (isset($response['error'])) {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'id' => null,
|
||||||
|
'error' => $response['error']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Zoho API error codes
|
||||||
|
if (isset($response['data'][0]['code']) && !in_array($response['data'][0]['code'], array('SUCCESS', 'DUPLICATE_DATA'))) {
|
||||||
|
$error_msg = isset($response['data'][0]['message']) ? $response['data'][0]['message'] : $response['data'][0]['code'];
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'id' => null,
|
||||||
|
'error' => $error_msg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for successful response with ID
|
||||||
|
if (isset($response['data'][0]['details']['id'])) {
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'id' => $response['data'][0]['details']['id'],
|
||||||
|
'error' => null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate record handling (also a success case)
|
||||||
|
if (isset($response['data'][0]['code']) && $response['data'][0]['code'] === 'DUPLICATE_DATA'
|
||||||
|
&& isset($response['data'][0]['details']['duplicate_record']['id'])) {
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'id' => $response['data'][0]['details']['duplicate_record']['id'],
|
||||||
|
'error' => null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for success status without ID (e.g., PUT updates)
|
||||||
|
if (isset($response['data'][0]['status']) && $response['data'][0]['status'] === 'success') {
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'id' => isset($response['data'][0]['details']['id']) ? $response['data'][0]['details']['id'] : null,
|
||||||
|
'error' => null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown response structure - treat as failure
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'id' => null,
|
||||||
|
'error' => 'Unexpected API response: ' . json_encode(array_slice($response, 0, 3))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a hash for sync data to detect changes
|
* Generate a hash for sync data to detect changes
|
||||||
|
|
@ -202,37 +282,42 @@ class HVAC_Zoho_Sync {
|
||||||
foreach ($events as $event) {
|
foreach ($events as $event) {
|
||||||
try {
|
try {
|
||||||
$campaign_data = $this->prepare_campaign_data($event);
|
$campaign_data = $this->prepare_campaign_data($event);
|
||||||
|
|
||||||
// Check if data has changed using hash comparison
|
// Check if data has changed using hash comparison
|
||||||
if (!$this->should_sync($event->ID, $campaign_data)) {
|
if (!$this->should_sync($event->ID, $campaign_data)) {
|
||||||
$results['skipped']++;
|
$results['skipped']++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$campaign_id = null;
|
$campaign_id = null;
|
||||||
|
$sync_succeeded = false;
|
||||||
|
|
||||||
// FIRST: Check if we already have a stored Zoho Campaign ID
|
// FIRST: Check if we already have a stored Zoho Campaign ID
|
||||||
$stored_campaign_id = get_post_meta($event->ID, '_zoho_campaign_id', true);
|
$stored_campaign_id = get_post_meta($event->ID, '_zoho_campaign_id', true);
|
||||||
|
|
||||||
if (!empty($stored_campaign_id)) {
|
if (!empty($stored_campaign_id)) {
|
||||||
// We have a stored ID - try to update
|
// We have a stored ID - try to update
|
||||||
$update_response = $this->auth->make_api_request("/Campaigns/{$stored_campaign_id}", 'PUT', array(
|
$update_response = $this->auth->make_api_request("/Campaigns/{$stored_campaign_id}", 'PUT', array(
|
||||||
'data' => array($campaign_data)
|
'data' => array($campaign_data)
|
||||||
));
|
));
|
||||||
|
$validated = $this->validate_api_response($update_response);
|
||||||
|
|
||||||
// Check if update failed due to invalid ID (e.g. campaign deleted in Zoho)
|
// Check if update failed due to invalid ID (e.g. campaign deleted in Zoho)
|
||||||
if (isset($update_response['code']) && $update_response['code'] === 'INVALID_DATA') {
|
if (!$validated['success'] && isset($update_response['code']) && $update_response['code'] === 'INVALID_DATA') {
|
||||||
// Fallback: Create new campaign
|
// Fallback: Create new campaign
|
||||||
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
|
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
|
||||||
'data' => array($campaign_data)
|
'data' => array($campaign_data)
|
||||||
));
|
));
|
||||||
|
$validated = $this->validate_api_response($create_response);
|
||||||
$results['responses'][] = array('type' => 'create_fallback', 'id' => $event->ID, 'response' => $create_response);
|
$results['responses'][] = array('type' => 'create_fallback', 'id' => $event->ID, 'response' => $create_response);
|
||||||
|
|
||||||
if (!empty($create_response['data'][0]['details']['id'])) {
|
if ($validated['success'] && $validated['id']) {
|
||||||
$campaign_id = $create_response['data'][0]['details']['id'];
|
$campaign_id = $validated['id'];
|
||||||
|
$sync_succeeded = true;
|
||||||
}
|
}
|
||||||
} else {
|
} elseif ($validated['success']) {
|
||||||
$campaign_id = $stored_campaign_id;
|
$campaign_id = $stored_campaign_id;
|
||||||
|
$sync_succeeded = true;
|
||||||
$results['responses'][] = array('type' => 'update', 'id' => $event->ID, 'response' => $update_response);
|
$results['responses'][] = array('type' => 'update', 'id' => $event->ID, 'response' => $update_response);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -240,25 +325,32 @@ class HVAC_Zoho_Sync {
|
||||||
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
|
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
|
||||||
'data' => array($campaign_data)
|
'data' => array($campaign_data)
|
||||||
));
|
));
|
||||||
|
$validated = $this->validate_api_response($create_response);
|
||||||
$results['responses'][] = array('type' => 'create', 'id' => $event->ID, 'response' => $create_response);
|
$results['responses'][] = array('type' => 'create', 'id' => $event->ID, 'response' => $create_response);
|
||||||
|
|
||||||
// Extract campaign ID from create response
|
if ($validated['success'] && $validated['id']) {
|
||||||
if (!empty($create_response['data'][0]['details']['id'])) {
|
$campaign_id = $validated['id'];
|
||||||
$campaign_id = $create_response['data'][0]['details']['id'];
|
$sync_succeeded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$results['synced']++;
|
if ($sync_succeeded) {
|
||||||
|
$results['synced']++;
|
||||||
// Update event meta with Zoho Campaign ID and sync hash
|
|
||||||
if (!empty($campaign_id)) {
|
// Only update hash and Zoho ID on confirmed success
|
||||||
update_post_meta($event->ID, '_zoho_campaign_id', $campaign_id);
|
if (!empty($campaign_id)) {
|
||||||
|
update_post_meta($event->ID, '_zoho_campaign_id', $campaign_id);
|
||||||
|
}
|
||||||
|
update_post_meta($event->ID, '_zoho_sync_hash', $this->generate_sync_hash($campaign_data));
|
||||||
|
} else {
|
||||||
|
$results['failed']++;
|
||||||
|
$error_msg = isset($validated['error']) ? $validated['error'] : 'Unknown API error';
|
||||||
|
$results['errors'][] = sprintf('Event %s: %s', $event->ID, $error_msg);
|
||||||
}
|
}
|
||||||
update_post_meta($event->ID, '_zoho_sync_hash', $this->generate_sync_hash($campaign_data));
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
} catch (Exception $e) {
|
|
||||||
$results['failed']++;
|
$results['failed']++;
|
||||||
$results['errors'][] = sprintf('Event %s: %s', $event->ID, $e->getMessage());
|
$results['errors'][] = sprintf('Event %s: [%s] %s', $event->ID, get_class($e), $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,46 +443,59 @@ class HVAC_Zoho_Sync {
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
try {
|
try {
|
||||||
$contact_data = $this->prepare_contact_data($user);
|
$contact_data = $this->prepare_contact_data($user);
|
||||||
|
|
||||||
// Check if data has changed using hash comparison
|
// Check if data has changed using hash comparison
|
||||||
if (!$this->should_sync_user($user->ID, $contact_data)) {
|
if (!$this->should_sync_user($user->ID, $contact_data)) {
|
||||||
$results['skipped']++;
|
$results['skipped']++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$contact_id = null;
|
||||||
|
$sync_succeeded = false;
|
||||||
|
|
||||||
// Check if contact already exists in Zoho
|
// Check if contact already exists in Zoho
|
||||||
$search_response = $this->auth->make_api_request('/Contacts/search', 'GET', array(
|
$search_response = $this->auth->make_api_request('/Contacts/search', 'GET', array(
|
||||||
'criteria' => "(Email:equals:{$contact_data['Email']})"
|
'criteria' => "(Email:equals:{$contact_data['Email']})"
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!empty($search_response['data'])) {
|
if (!empty($search_response['data'])) {
|
||||||
// Update existing contact
|
// Update existing contact
|
||||||
$contact_id = $search_response['data'][0]['id'];
|
$contact_id = $search_response['data'][0]['id'];
|
||||||
$update_response = $this->auth->make_api_request("/Contacts/{$contact_id}", 'PUT', array(
|
$update_response = $this->auth->make_api_request("/Contacts/{$contact_id}", 'PUT', array(
|
||||||
'data' => array($contact_data)
|
'data' => array($contact_data)
|
||||||
));
|
));
|
||||||
|
$validated = $this->validate_api_response($update_response);
|
||||||
|
$sync_succeeded = $validated['success'];
|
||||||
} else {
|
} else {
|
||||||
// Create new contact
|
// Create new contact
|
||||||
$create_response = $this->auth->make_api_request('/Contacts', 'POST', array(
|
$create_response = $this->auth->make_api_request('/Contacts', 'POST', array(
|
||||||
'data' => array($contact_data)
|
'data' => array($contact_data)
|
||||||
));
|
));
|
||||||
|
$validated = $this->validate_api_response($create_response);
|
||||||
if (!empty($create_response['data'][0]['details']['id'])) {
|
$sync_succeeded = $validated['success'];
|
||||||
$contact_id = $create_response['data'][0]['details']['id'];
|
|
||||||
|
if ($validated['success'] && $validated['id']) {
|
||||||
|
$contact_id = $validated['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$results['synced']++;
|
if ($sync_succeeded) {
|
||||||
|
$results['synced']++;
|
||||||
// Update user meta with Zoho ID and sync hash
|
|
||||||
if (isset($contact_id)) {
|
// Only update hash and Zoho ID on confirmed success
|
||||||
update_user_meta($user->ID, '_zoho_contact_id', $contact_id);
|
if (!empty($contact_id)) {
|
||||||
|
update_user_meta($user->ID, '_zoho_contact_id', $contact_id);
|
||||||
|
}
|
||||||
|
update_user_meta($user->ID, '_zoho_sync_hash', $this->generate_sync_hash($contact_data));
|
||||||
|
} else {
|
||||||
|
$results['failed']++;
|
||||||
|
$error_msg = isset($validated['error']) ? $validated['error'] : 'Unknown API error';
|
||||||
|
$results['errors'][] = sprintf('User %s: %s', $user->ID, $error_msg);
|
||||||
}
|
}
|
||||||
update_user_meta($user->ID, '_zoho_sync_hash', $this->generate_sync_hash($contact_data));
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
} catch (Exception $e) {
|
|
||||||
$results['failed']++;
|
$results['failed']++;
|
||||||
$results['errors'][] = sprintf('User %s: %s', $user->ID, $e->getMessage());
|
$results['errors'][] = sprintf('User %s: [%s] %s', $user->ID, get_class($e), $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,13 +594,16 @@ class HVAC_Zoho_Sync {
|
||||||
foreach ($orders as $order) {
|
foreach ($orders as $order) {
|
||||||
try {
|
try {
|
||||||
$invoice_data = $this->prepare_tc_invoice_data($order);
|
$invoice_data = $this->prepare_tc_invoice_data($order);
|
||||||
|
|
||||||
// Check if data has changed using hash comparison
|
// Check if data has changed using hash comparison
|
||||||
if (!$this->should_sync($order->ID, $invoice_data)) {
|
if (!$this->should_sync($order->ID, $invoice_data)) {
|
||||||
$results['skipped']++;
|
$results['skipped']++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$invoice_id = null;
|
||||||
|
$sync_succeeded = false;
|
||||||
|
|
||||||
// Check if invoice already exists in Zoho (by WordPress Order ID)
|
// Check if invoice already exists in Zoho (by WordPress Order ID)
|
||||||
$search_response = $this->auth->make_api_request(
|
$search_response = $this->auth->make_api_request(
|
||||||
'/Invoices/search?criteria=(WordPress_Order_ID:equals:' . $order->ID . ')',
|
'/Invoices/search?criteria=(WordPress_Order_ID:equals:' . $order->ID . ')',
|
||||||
|
|
@ -505,31 +613,41 @@ class HVAC_Zoho_Sync {
|
||||||
if (!empty($search_response['data'])) {
|
if (!empty($search_response['data'])) {
|
||||||
// Update existing invoice
|
// Update existing invoice
|
||||||
$invoice_id = $search_response['data'][0]['id'];
|
$invoice_id = $search_response['data'][0]['id'];
|
||||||
$this->auth->make_api_request("/Invoices/{$invoice_id}", 'PUT', array(
|
$update_response = $this->auth->make_api_request("/Invoices/{$invoice_id}", 'PUT', array(
|
||||||
'data' => array($invoice_data)
|
'data' => array($invoice_data)
|
||||||
));
|
));
|
||||||
|
$validated = $this->validate_api_response($update_response);
|
||||||
|
$sync_succeeded = $validated['success'];
|
||||||
} else {
|
} else {
|
||||||
// Create new invoice
|
// Create new invoice
|
||||||
$create_response = $this->auth->make_api_request('/Invoices', 'POST', array(
|
$create_response = $this->auth->make_api_request('/Invoices', 'POST', array(
|
||||||
'data' => array($invoice_data)
|
'data' => array($invoice_data)
|
||||||
));
|
));
|
||||||
|
$validated = $this->validate_api_response($create_response);
|
||||||
|
$sync_succeeded = $validated['success'];
|
||||||
|
|
||||||
if (!empty($create_response['data'][0]['details']['id'])) {
|
if ($validated['success'] && $validated['id']) {
|
||||||
$invoice_id = $create_response['data'][0]['details']['id'];
|
$invoice_id = $validated['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$results['synced']++;
|
if ($sync_succeeded) {
|
||||||
|
$results['synced']++;
|
||||||
|
|
||||||
// Update order meta with Zoho ID and sync hash
|
// Only update hash and Zoho ID on confirmed success
|
||||||
if (isset($invoice_id)) {
|
if (!empty($invoice_id)) {
|
||||||
update_post_meta($order->ID, '_zoho_invoice_id', $invoice_id);
|
update_post_meta($order->ID, '_zoho_invoice_id', $invoice_id);
|
||||||
|
}
|
||||||
|
update_post_meta($order->ID, '_zoho_sync_hash', $this->generate_sync_hash($invoice_data));
|
||||||
|
} else {
|
||||||
|
$results['failed']++;
|
||||||
|
$error_msg = isset($validated['error']) ? $validated['error'] : 'Unknown API error';
|
||||||
|
$results['errors'][] = sprintf('Order %s: %s', $order->ID, $error_msg);
|
||||||
}
|
}
|
||||||
update_post_meta($order->ID, '_zoho_sync_hash', $this->generate_sync_hash($invoice_data));
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
$results['failed']++;
|
$results['failed']++;
|
||||||
$results['errors'][] = sprintf('Order %s: %s', $order->ID, $e->getMessage());
|
$results['errors'][] = sprintf('Order %s: [%s] %s', $order->ID, get_class($e), $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -687,10 +805,20 @@ class HVAC_Zoho_Sync {
|
||||||
$results['responses'][] = array('type' => 'debug', 'msg' => "Debug: Attendee {$attendee->ID} found Contact ID: " . $cid_debug);
|
$results['responses'][] = array('type' => 'debug', 'msg' => "Debug: Attendee {$attendee->ID} found Contact ID: " . $cid_debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If contact creation failed, count as failed and skip hash update
|
||||||
|
if (!$contact_id) {
|
||||||
|
$results['failed']++;
|
||||||
|
$error_detail = $this->last_contact_error ?: 'Unknown error';
|
||||||
|
$results['errors'][] = sprintf('Attendee %s: No contact_id created. %s', $attendee->ID, $error_detail);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attendee_sync_ok = true;
|
||||||
|
|
||||||
// Step 2: Create Campaign Member (link Contact to Campaign)
|
// Step 2: Create Campaign Member (link Contact to Campaign)
|
||||||
if ($contact_id && !empty($attendee_data['event_id'])) {
|
if (!empty($attendee_data['event_id'])) {
|
||||||
$campaign_id = get_post_meta($attendee_data['event_id'], '_zoho_campaign_id', true);
|
$campaign_id = get_post_meta($attendee_data['event_id'], '_zoho_campaign_id', true);
|
||||||
|
|
||||||
// Debug: Log event_id and campaign_id for troubleshooting
|
// Debug: Log event_id and campaign_id for troubleshooting
|
||||||
if (count($results['responses']) < 10) {
|
if (count($results['responses']) < 10) {
|
||||||
$results['responses'][] = array(
|
$results['responses'][] = array(
|
||||||
|
|
@ -700,10 +828,10 @@ class HVAC_Zoho_Sync {
|
||||||
'campaign_id' => $campaign_id ?: 'NOT_SET'
|
'campaign_id' => $campaign_id ?: 'NOT_SET'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($campaign_id) {
|
if ($campaign_id) {
|
||||||
$assoc_response = $this->create_campaign_member($contact_id, $campaign_id, 'Attended');
|
$assoc_response = $this->create_campaign_member($contact_id, $campaign_id, 'Attended');
|
||||||
|
|
||||||
// ALWAYS capture the first link attempt for debugging
|
// ALWAYS capture the first link attempt for debugging
|
||||||
if (!isset($results['first_link_attempt'])) {
|
if (!isset($results['first_link_attempt'])) {
|
||||||
$results['first_link_attempt'] = array(
|
$results['first_link_attempt'] = array(
|
||||||
|
|
@ -713,7 +841,7 @@ class HVAC_Zoho_Sync {
|
||||||
'response' => $assoc_response
|
'response' => $assoc_response
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: Add responses (increased limit to 20)
|
// Debug: Add responses (increased limit to 20)
|
||||||
if (count($results['responses']) < 20) {
|
if (count($results['responses']) < 20) {
|
||||||
$results['responses'][] = array('type' => 'link_attempt', 'id' => $attendee->ID, 'response' => $assoc_response);
|
$results['responses'][] = array('type' => 'link_attempt', 'id' => $attendee->ID, 'response' => $assoc_response);
|
||||||
|
|
@ -732,9 +860,6 @@ class HVAC_Zoho_Sync {
|
||||||
} else {
|
} else {
|
||||||
$results['errors'][] = sprintf('Attendee %s: Event %s has no Zoho Campaign ID', $attendee->ID, $attendee_data['event_id']);
|
$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'])) {
|
} elseif (empty($attendee_data['event_id'])) {
|
||||||
// Debug: Log when event_id is missing
|
// Debug: Log when event_id is missing
|
||||||
if (count($results['responses']) < 10) {
|
if (count($results['responses']) < 10) {
|
||||||
|
|
@ -742,9 +867,10 @@ class HVAC_Zoho_Sync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contact was created/found successfully - count as synced
|
||||||
$results['synced']++;
|
$results['synced']++;
|
||||||
|
|
||||||
// Update attendee meta with Zoho Contact ID and sync hash
|
// Only update hash on confirmed contact success
|
||||||
update_post_meta($attendee->ID, '_zoho_contact_id', $contact_id);
|
update_post_meta($attendee->ID, '_zoho_contact_id', $contact_id);
|
||||||
update_post_meta($attendee->ID, '_zoho_sync_hash', $this->generate_sync_hash($attendee_data));
|
update_post_meta($attendee->ID, '_zoho_sync_hash', $this->generate_sync_hash($attendee_data));
|
||||||
|
|
||||||
|
|
@ -851,7 +977,7 @@ class HVAC_Zoho_Sync {
|
||||||
foreach ($rsvps as $rsvp) {
|
foreach ($rsvps as $rsvp) {
|
||||||
try {
|
try {
|
||||||
$rsvp_data = $this->prepare_rsvp_data($rsvp);
|
$rsvp_data = $this->prepare_rsvp_data($rsvp);
|
||||||
|
|
||||||
// Check if data has changed using hash comparison
|
// Check if data has changed using hash comparison
|
||||||
if (!$this->should_sync($rsvp->ID, $rsvp_data)) {
|
if (!$this->should_sync($rsvp->ID, $rsvp_data)) {
|
||||||
$results['skipped']++;
|
$results['skipped']++;
|
||||||
|
|
@ -866,12 +992,17 @@ class HVAC_Zoho_Sync {
|
||||||
|
|
||||||
// Step 1: Create/Update Lead
|
// Step 1: Create/Update Lead
|
||||||
$lead_id = $this->ensure_lead_exists($rsvp_data);
|
$lead_id = $this->ensure_lead_exists($rsvp_data);
|
||||||
if ($lead_id) {
|
|
||||||
$results['leads_created']++;
|
if (!$lead_id) {
|
||||||
|
$results['failed']++;
|
||||||
|
$results['errors'][] = sprintf('RSVP %s: Failed to create/find lead', $rsvp->ID);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$results['leads_created']++;
|
||||||
|
|
||||||
// Step 2: Create Campaign Member (link Lead to Campaign)
|
// Step 2: Create Campaign Member (link Lead to Campaign)
|
||||||
if ($lead_id && !empty($rsvp_data['event_id'])) {
|
if (!empty($rsvp_data['event_id'])) {
|
||||||
$campaign_id = get_post_meta($rsvp_data['event_id'], '_zoho_campaign_id', true);
|
$campaign_id = get_post_meta($rsvp_data['event_id'], '_zoho_campaign_id', true);
|
||||||
if ($campaign_id) {
|
if ($campaign_id) {
|
||||||
$assoc_response = $this->create_campaign_member($lead_id, $campaign_id, 'Responded', 'Leads');
|
$assoc_response = $this->create_campaign_member($lead_id, $campaign_id, 'Responded', 'Leads');
|
||||||
|
|
@ -881,7 +1012,7 @@ class HVAC_Zoho_Sync {
|
||||||
$results['campaign_members_created']++;
|
$results['campaign_members_created']++;
|
||||||
} else {
|
} else {
|
||||||
$results['errors'][] = sprintf('RSVP %s: Failed to link to campaign. Response: %s', $rsvp->ID, json_encode($assoc_response));
|
$results['errors'][] = sprintf('RSVP %s: Failed to link to campaign. Response: %s', $rsvp->ID, json_encode($assoc_response));
|
||||||
if (isset($assoc_response['data'])) {
|
if (isset($assoc_response['data'])) {
|
||||||
$results['responses'][] = array('type' => 'link_error', 'id' => $rsvp->ID, 'response' => $assoc_response);
|
$results['responses'][] = array('type' => 'link_error', 'id' => $rsvp->ID, 'response' => $assoc_response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -892,13 +1023,13 @@ class HVAC_Zoho_Sync {
|
||||||
|
|
||||||
$results['synced']++;
|
$results['synced']++;
|
||||||
|
|
||||||
// Update RSVP meta with Zoho Lead ID and sync hash
|
// Only update hash on confirmed lead creation success
|
||||||
update_post_meta($rsvp->ID, '_zoho_lead_id', $lead_id);
|
update_post_meta($rsvp->ID, '_zoho_lead_id', $lead_id);
|
||||||
update_post_meta($rsvp->ID, '_zoho_sync_hash', $this->generate_sync_hash($rsvp_data));
|
update_post_meta($rsvp->ID, '_zoho_sync_hash', $this->generate_sync_hash($rsvp_data));
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
$results['failed']++;
|
$results['failed']++;
|
||||||
$results['errors'][] = sprintf('RSVP %s: %s', $rsvp->ID, $e->getMessage());
|
$results['errors'][] = sprintf('RSVP %s: [%s] %s', $rsvp->ID, get_class($e), $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue