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:
ben 2026-02-06 11:25:26 -04:00
parent 4b53d3eab6
commit 03b9bce52d
6 changed files with 416 additions and 106 deletions

View file

@ -1,8 +1,8 @@
# HVAC Community Events - Project Status
**Last Updated:** February 6, 2026
**Current Session:** Near Me Button Mobile Fix
**Version:** 2.2.6 (Deployed to Staging)
**Current Session:** Zoho CRM Sync Fix
**Version:** 2.2.6 (Deployed to Production)
---
@ -18,9 +18,84 @@
- Contact forms (trainer, venue)
- 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**

View file

@ -220,6 +220,13 @@ jQuery(document).ready(function ($) {
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
if (response.data.debug) {
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
// =====================================================

View file

@ -47,6 +47,8 @@ class HVAC_Zoho_Admin {
add_action('wp_ajax_hvac_zoho_run_scheduled_sync', array($this, 'run_scheduled_sync_now'));
// Add simple test handler
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_action('init', array($this, 'add_oauth_rewrite_rule'), 5);
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">
<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">
<h3>Events Campaigns</h3>
<p>Sync events from The Events Calendar to Zoho CRM Campaigns</p>
@ -897,20 +906,28 @@ class HVAC_Zoho_Admin {
}
// Success!
// Success!
wp_send_json_success(array(
$mode_info = HVAC_Zoho_CRM_Auth::get_debug_mode_info();
$response_data = array(
'message' => 'Connection successful!',
'modules' => isset($response['modules']) ? count($response['modules']) . ' modules available' : 'API connected',
'client_id' => substr($client_id, 0, 10) . '...',
'client_secret_exists' => true,
'refresh_token_exists' => true,
'is_staging' => $mode_info['is_staging'],
'mode_info' => $mode_info,
'credentials_status' => array(
'client_id' => substr($client_id, 0, 10) . '...',
'client_secret_exists' => true,
'refresh_token_exists' => 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) {
$error_response = array(
'message' => 'Connection test failed due to exception',
@ -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,
));
}
}
?>

View file

@ -112,10 +112,10 @@ final class HVAC_Plugin {
*/
private function defineConstants(): void {
if (!defined('HVAC_PLUGIN_VERSION')) {
define('HVAC_PLUGIN_VERSION', '2.0.0');
define('HVAC_PLUGIN_VERSION', '2.2.11');
}
if (!defined('HVAC_VERSION')) {
define('HVAC_VERSION', '2.2.6');
define('HVAC_VERSION', '2.2.11');
}
if (!defined('HVAC_PLUGIN_FILE')) {
define('HVAC_PLUGIN_FILE', dirname(__DIR__) . '/hvac-community-events.php');
@ -176,6 +176,9 @@ final class HVAC_Plugin {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-browser-detection.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-shortcodes.php';
// DISABLED - Using TEC Community Events 5.x instead

View file

@ -222,18 +222,21 @@ class HVAC_Zoho_CRM_Auth {
return true;
}
// 3. Parse hostname from site URL for accurate comparison
$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 ||
strpos($site_url, 'dev') !== false ||
strpos($site_url, 'test') !== false ||
strpos($site_url, 'cloudwaysapps.com') !== false) {
return true;
if (empty($host)) {
return true; // Can't determine host, default to staging for safety
}
// 4. Default check: Production only on upskillhvac.com
return strpos($site_url, 'upskillhvac.com') === false;
// 4. Production: upskillhvac.com or www.upskillhvac.com
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
*/
public static function get_debug_mode_info() {
$site_url = get_site_url();
$host = wp_parse_url($site_url, PHP_URL_HOST);
$info = array(
'site_url' => get_site_url(),
'site_url' => $site_url,
'parsed_host' => $host,
'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,
@ -255,20 +262,12 @@ class HVAC_Zoho_CRM_Auth {
$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';
} 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 {
$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"';
}
}
$info['detection_logic'][] = 'STAGING: Hostname "' . $host . '" is not upskillhvac.com';
}
return $info;
@ -283,7 +282,7 @@ class HVAC_Zoho_CRM_Auth {
// In staging mode, only allow read operations, no writes
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(
'data' => array(
array(
@ -291,8 +290,8 @@ class HVAC_Zoho_CRM_Auth {
'details' => array(
'message' => 'Staging mode active. Write operations are disabled.'
),
'message' => 'This would have been a ' . $method . ' request to: ' . $endpoint,
'status' => 'success'
'message' => 'Blocked ' . $method . ' request to: ' . $endpoint,
'status' => 'skipped_staging'
)
)
);

View file

@ -56,6 +56,86 @@ class HVAC_Zoho_Sync {
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
*
@ -210,6 +290,7 @@ class HVAC_Zoho_Sync {
}
$campaign_id = null;
$sync_succeeded = false;
// FIRST: Check if we already have a stored Zoho Campaign ID
$stored_campaign_id = get_post_meta($event->ID, '_zoho_campaign_id', true);
@ -219,20 +300,24 @@ class HVAC_Zoho_Sync {
$update_response = $this->auth->make_api_request("/Campaigns/{$stored_campaign_id}", 'PUT', array(
'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)
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
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
'data' => array($campaign_data)
));
$validated = $this->validate_api_response($create_response);
$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'];
if ($validated['success'] && $validated['id']) {
$campaign_id = $validated['id'];
$sync_succeeded = true;
}
} else {
} elseif ($validated['success']) {
$campaign_id = $stored_campaign_id;
$sync_succeeded = true;
$results['responses'][] = array('type' => 'update', 'id' => $event->ID, 'response' => $update_response);
}
} else {
@ -240,25 +325,32 @@ class HVAC_Zoho_Sync {
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
'data' => array($campaign_data)
));
$validated = $this->validate_api_response($create_response);
$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'];
if ($validated['success'] && $validated['id']) {
$campaign_id = $validated['id'];
$sync_succeeded = true;
}
}
if ($sync_succeeded) {
$results['synced']++;
// Update event meta with Zoho Campaign ID and sync hash
// Only update hash and Zoho ID on confirmed success
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));
} catch (Exception $e) {
} else {
$results['failed']++;
$results['errors'][] = sprintf('Event %s: %s', $event->ID, $e->getMessage());
$error_msg = isset($validated['error']) ? $validated['error'] : 'Unknown API error';
$results['errors'][] = sprintf('Event %s: %s', $event->ID, $error_msg);
}
} catch (\Throwable $e) {
$results['failed']++;
$results['errors'][] = sprintf('Event %s: [%s] %s', $event->ID, get_class($e), $e->getMessage());
}
}
@ -358,6 +450,9 @@ class HVAC_Zoho_Sync {
continue;
}
$contact_id = null;
$sync_succeeded = false;
// Check if contact already exists in Zoho
$search_response = $this->auth->make_api_request('/Contacts/search', 'GET', array(
'criteria' => "(Email:equals:{$contact_data['Email']})"
@ -369,28 +464,38 @@ class HVAC_Zoho_Sync {
$update_response = $this->auth->make_api_request("/Contacts/{$contact_id}", 'PUT', array(
'data' => array($contact_data)
));
$validated = $this->validate_api_response($update_response);
$sync_succeeded = $validated['success'];
} else {
// Create new contact
$create_response = $this->auth->make_api_request('/Contacts', 'POST', array(
'data' => array($contact_data)
));
$validated = $this->validate_api_response($create_response);
$sync_succeeded = $validated['success'];
if (!empty($create_response['data'][0]['details']['id'])) {
$contact_id = $create_response['data'][0]['details']['id'];
if ($validated['success'] && $validated['id']) {
$contact_id = $validated['id'];
}
}
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
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));
} catch (Exception $e) {
} else {
$results['failed']++;
$results['errors'][] = sprintf('User %s: %s', $user->ID, $e->getMessage());
$error_msg = isset($validated['error']) ? $validated['error'] : 'Unknown API error';
$results['errors'][] = sprintf('User %s: %s', $user->ID, $error_msg);
}
} catch (\Throwable $e) {
$results['failed']++;
$results['errors'][] = sprintf('User %s: [%s] %s', $user->ID, get_class($e), $e->getMessage());
}
}
@ -496,6 +601,9 @@ class HVAC_Zoho_Sync {
continue;
}
$invoice_id = null;
$sync_succeeded = false;
// Check if invoice already exists in Zoho (by WordPress Order ID)
$search_response = $this->auth->make_api_request(
'/Invoices/search?criteria=(WordPress_Order_ID:equals:' . $order->ID . ')',
@ -505,31 +613,41 @@ class HVAC_Zoho_Sync {
if (!empty($search_response['data'])) {
// Update existing invoice
$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)
));
$validated = $this->validate_api_response($update_response);
$sync_succeeded = $validated['success'];
} else {
// Create new invoice
$create_response = $this->auth->make_api_request('/Invoices', 'POST', array(
'data' => array($invoice_data)
));
$validated = $this->validate_api_response($create_response);
$sync_succeeded = $validated['success'];
if (!empty($create_response['data'][0]['details']['id'])) {
$invoice_id = $create_response['data'][0]['details']['id'];
if ($validated['success'] && $validated['id']) {
$invoice_id = $validated['id'];
}
}
if ($sync_succeeded) {
$results['synced']++;
// Update order meta with Zoho ID and sync hash
if (isset($invoice_id)) {
// Only update hash and Zoho ID on confirmed success
if (!empty($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));
} catch (Exception $e) {
} else {
$results['failed']++;
$results['errors'][] = sprintf('Order %s: %s', $order->ID, $e->getMessage());
$error_msg = isset($validated['error']) ? $validated['error'] : 'Unknown API error';
$results['errors'][] = sprintf('Order %s: %s', $order->ID, $error_msg);
}
} catch (\Throwable $e) {
$results['failed']++;
$results['errors'][] = sprintf('Order %s: [%s] %s', $order->ID, get_class($e), $e->getMessage());
}
}
@ -687,8 +805,18 @@ class HVAC_Zoho_Sync {
$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)
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);
// Debug: Log event_id and campaign_id for troubleshooting
@ -732,9 +860,6 @@ class HVAC_Zoho_Sync {
} 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) {
@ -742,9 +867,10 @@ class HVAC_Zoho_Sync {
}
}
// Contact was created/found successfully - count as 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_sync_hash', $this->generate_sync_hash($attendee_data));
@ -866,12 +992,17 @@ class HVAC_Zoho_Sync {
// Step 1: Create/Update Lead
$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)
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);
if ($campaign_id) {
$assoc_response = $this->create_campaign_member($lead_id, $campaign_id, 'Responded', 'Leads');
@ -892,13 +1023,13 @@ class HVAC_Zoho_Sync {
$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_sync_hash', $this->generate_sync_hash($rsvp_data));
} catch (Exception $e) {
} catch (\Throwable $e) {
$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());
}
}