feat(zoho): Add hash-based change detection to prevent re-syncing unchanged records
Some checks failed
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Has been cancelled
Security Monitoring & Compliance / Secrets & Credential Scan (push) Has been cancelled
Security Monitoring & Compliance / WordPress Security Analysis (push) Has been cancelled
Security Monitoring & Compliance / Static Code Security Analysis (push) Has been cancelled
Security Monitoring & Compliance / Security Compliance Validation (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Notification (push) Has been cancelled
Security Monitoring & Compliance / Security Summary Report (push) Has been cancelled
Security Monitoring & Compliance / Security Team Notification (push) Has been cancelled

- Add generate_sync_hash(), should_sync(), and should_sync_user() helper methods
- Modify all 5 sync methods to check hashes before syncing
- Add 'skipped' count to track unchanged records
- Update scheduled sync to aggregate and log skipped counts

This fixes the issue where 59 items were synced every scheduled run even
when no WordPress records had changed.
This commit is contained in:
ben 2025-12-23 16:15:15 -04:00
parent f464224cd8
commit 1526d9f23b
2 changed files with 93 additions and 7 deletions

View file

@ -261,11 +261,15 @@ class HVAC_Zoho_Scheduled_Sync {
// Calculate totals
$total_synced = 0;
$total_skipped = 0;
$total_failed = 0;
foreach (array('events', 'users', 'attendees', 'rsvps', 'purchases') as $type) {
if (isset($results[$type]['synced'])) {
$total_synced += $results[$type]['synced'];
}
if (isset($results[$type]['skipped'])) {
$total_skipped += $results[$type]['skipped'];
}
if (isset($results[$type]['failed'])) {
$total_failed += $results[$type]['failed'];
}
@ -274,13 +278,14 @@ class HVAC_Zoho_Scheduled_Sync {
$results['completed_at'] = date('Y-m-d H:i:s');
$results['duration_seconds'] = time() - $start_time;
$results['total_synced'] = $total_synced;
$results['total_skipped'] = $total_skipped;
$results['total_failed'] = $total_failed;
// Save last result
update_option(self::OPTION_LAST_RESULT, $results);
if (class_exists('HVAC_Logger')) {
HVAC_Logger::info("Scheduled sync completed: {$total_synced} synced, {$total_failed} failed", 'ZohoScheduledSync');
HVAC_Logger::info("Scheduled sync completed: {$total_synced} synced, {$total_skipped} skipped, {$total_failed} failed", 'ZohoScheduledSync');
}
return $results;
@ -300,6 +305,7 @@ class HVAC_Zoho_Scheduled_Sync {
$aggregated = array(
'total' => 0,
'synced' => 0,
'skipped' => 0,
'failed' => 0,
'errors' => array(),
);
@ -311,6 +317,7 @@ class HVAC_Zoho_Scheduled_Sync {
// Aggregate results
$aggregated['total'] = $result['total'] ?? 0;
$aggregated['synced'] += $result['synced'] ?? 0;
$aggregated['skipped'] += $result['skipped'] ?? 0;
$aggregated['failed'] += $result['failed'] ?? 0;
if (!empty($result['errors'])) {

View file

@ -56,6 +56,45 @@ class HVAC_Zoho_Sync {
return !HVAC_Zoho_CRM_Auth::is_staging_mode();
}
/**
* Generate a hash for sync data to detect changes
*
* @param array $data Data being synced
* @return string MD5 hash
*/
private function generate_sync_hash($data) {
// Sort keys for consistent hashing
ksort($data);
return md5(json_encode($data));
}
/**
* Check if a post record should be synced based on hash comparison
*
* @param int $post_id Post ID
* @param array $data Data being synced
* @param string $meta_key Meta key for storing hash (default: _zoho_sync_hash)
* @return bool True if data has changed and should be synced
*/
private function should_sync($post_id, $data, $meta_key = '_zoho_sync_hash') {
$new_hash = $this->generate_sync_hash($data);
$old_hash = get_post_meta($post_id, $meta_key, true);
return $new_hash !== $old_hash;
}
/**
* Check if a user should be synced based on hash comparison
*
* @param int $user_id User ID
* @param array $data Data being synced
* @return bool True if data has changed and should be synced
*/
private function should_sync_user($user_id, $data) {
$new_hash = $this->generate_sync_hash($data);
$old_hash = get_user_meta($user_id, '_zoho_sync_hash', true);
return $new_hash !== $old_hash;
}
/**
* Sync events to Zoho Campaigns
*
@ -68,6 +107,7 @@ class HVAC_Zoho_Sync {
$results = array(
'total' => 0,
'synced' => 0,
'skipped' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging,
@ -162,6 +202,13 @@ class HVAC_Zoho_Sync {
foreach ($events as $event) {
try {
$campaign_data = $this->prepare_campaign_data($event);
// Check if data has changed using hash comparison
if (!$this->should_sync($event->ID, $campaign_data)) {
$results['skipped']++;
continue;
}
$campaign_id = null;
// FIRST: Check if we already have a stored Zoho Campaign ID
@ -203,10 +250,11 @@ class HVAC_Zoho_Sync {
$results['synced']++;
// Update event meta with Zoho Campaign ID
// Update event meta with Zoho Campaign ID and sync hash
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) {
$results['failed']++;
@ -229,6 +277,7 @@ class HVAC_Zoho_Sync {
$results = array(
'total' => 0,
'synced' => 0,
'skipped' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging,
@ -303,6 +352,12 @@ class HVAC_Zoho_Sync {
try {
$contact_data = $this->prepare_contact_data($user);
// Check if data has changed using hash comparison
if (!$this->should_sync_user($user->ID, $contact_data)) {
$results['skipped']++;
continue;
}
// Check if contact already exists in Zoho
$search_response = $this->auth->make_api_request('/Contacts/search', 'GET', array(
'criteria' => "(Email:equals:{$contact_data['Email']})"
@ -327,10 +382,11 @@ class HVAC_Zoho_Sync {
$results['synced']++;
// Update user meta with Zoho ID
// Update user meta with Zoho ID and sync hash
if (isset($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) {
$results['failed']++;
@ -353,6 +409,7 @@ class HVAC_Zoho_Sync {
$results = array(
'total' => 0,
'synced' => 0,
'skipped' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging,
@ -432,6 +489,12 @@ class HVAC_Zoho_Sync {
foreach ($orders as $order) {
try {
$invoice_data = $this->prepare_tc_invoice_data($order);
// Check if data has changed using hash comparison
if (!$this->should_sync($order->ID, $invoice_data)) {
$results['skipped']++;
continue;
}
// Check if invoice already exists in Zoho (by WordPress Order ID)
$search_response = $this->auth->make_api_request(
@ -458,10 +521,11 @@ class HVAC_Zoho_Sync {
$results['synced']++;
// Update order meta with Zoho ID
// Update order meta with Zoho ID and sync hash
if (isset($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) {
$results['failed']++;
@ -484,6 +548,7 @@ class HVAC_Zoho_Sync {
$results = array(
'total' => 0,
'synced' => 0,
'skipped' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging,
@ -596,13 +661,18 @@ class HVAC_Zoho_Sync {
try {
$attendee_data = $this->prepare_attendee_data($attendee);
// Check if data has changed using hash comparison
if (!$this->should_sync($attendee->ID, $attendee_data)) {
$results['skipped']++;
continue;
}
// 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;
}
@ -674,8 +744,9 @@ class HVAC_Zoho_Sync {
$results['synced']++;
// Update attendee meta with Zoho Contact ID
// Update attendee meta with Zoho Contact ID and sync hash
update_post_meta($attendee->ID, '_zoho_contact_id', $contact_id);
update_post_meta($attendee->ID, '_zoho_sync_hash', $this->generate_sync_hash($attendee_data));
} catch (\Throwable $e) {
// Catch both Exception and Error (PHP 7+)
@ -703,6 +774,7 @@ class HVAC_Zoho_Sync {
$results = array(
'total' => 0,
'synced' => 0,
'skipped' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging,
@ -779,6 +851,12 @@ class HVAC_Zoho_Sync {
foreach ($rsvps as $rsvp) {
try {
$rsvp_data = $this->prepare_rsvp_data($rsvp);
// Check if data has changed using hash comparison
if (!$this->should_sync($rsvp->ID, $rsvp_data)) {
$results['skipped']++;
continue;
}
if (empty($rsvp_data['email'])) {
$results['failed']++;
@ -814,8 +892,9 @@ class HVAC_Zoho_Sync {
$results['synced']++;
// Update RSVP meta with Zoho Lead ID
// Update RSVP meta with Zoho Lead ID and sync hash
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) {
$results['failed']++;