From 1526d9f23b5b26741e5d7a642d2642db5c614415 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 23 Dec 2025 16:15:15 -0400 Subject: [PATCH] feat(zoho): Add hash-based change detection to prevent re-syncing unchanged records - 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. --- includes/zoho/class-zoho-scheduled-sync.php | 9 +- includes/zoho/class-zoho-sync.php | 91 +++++++++++++++++++-- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/includes/zoho/class-zoho-scheduled-sync.php b/includes/zoho/class-zoho-scheduled-sync.php index 409f7e84..792e6dd0 100644 --- a/includes/zoho/class-zoho-scheduled-sync.php +++ b/includes/zoho/class-zoho-scheduled-sync.php @@ -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'])) { diff --git a/includes/zoho/class-zoho-sync.php b/includes/zoho/class-zoho-sync.php index e714bf81..59c5b041 100644 --- a/includes/zoho/class-zoho-sync.php +++ b/includes/zoho/class-zoho-sync.php @@ -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']++;