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.
396 lines
12 KiB
PHP
396 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Zoho CRM Scheduled Sync Handler
|
|
*
|
|
* Manages WP-Cron based scheduled sync of WordPress data to Zoho CRM.
|
|
*
|
|
* @package HVACCommunityEvents
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Scheduled Sync Class
|
|
*/
|
|
class HVAC_Zoho_Scheduled_Sync {
|
|
|
|
/**
|
|
* Instance of this class
|
|
*
|
|
* @var HVAC_Zoho_Scheduled_Sync
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Cron hook name
|
|
*/
|
|
const CRON_HOOK = 'hvac_zoho_scheduled_sync';
|
|
|
|
/**
|
|
* Option names
|
|
*/
|
|
const OPTION_ENABLED = 'hvac_zoho_auto_sync';
|
|
const OPTION_INTERVAL = 'hvac_zoho_sync_frequency';
|
|
const OPTION_LAST_SYNC = 'hvac_zoho_last_sync_time';
|
|
const OPTION_LAST_RESULT = 'hvac_zoho_last_sync_result';
|
|
|
|
/**
|
|
* Get instance of this class
|
|
*
|
|
* @return HVAC_Zoho_Scheduled_Sync
|
|
*/
|
|
public static function instance() {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
// Register custom cron schedules
|
|
add_filter('cron_schedules', array($this, 'register_cron_schedules'));
|
|
|
|
// Register the cron action
|
|
add_action(self::CRON_HOOK, array($this, 'run_scheduled_sync'));
|
|
|
|
// Check if we need to reschedule on settings change
|
|
// Hook into both add_option (first time) and update_option (subsequent changes)
|
|
add_action('add_option_' . self::OPTION_ENABLED, array($this, 'on_setting_added'), 10, 2);
|
|
add_action('update_option_' . self::OPTION_ENABLED, array($this, 'on_setting_change'), 10, 2);
|
|
add_action('update_option_' . self::OPTION_INTERVAL, array($this, 'on_interval_change'), 10, 2);
|
|
}
|
|
|
|
/**
|
|
* Register custom cron schedules
|
|
*
|
|
* @param array $schedules Existing schedules
|
|
* @return array Modified schedules
|
|
*/
|
|
public function register_cron_schedules($schedules) {
|
|
$schedules['every_5_minutes'] = array(
|
|
'interval' => 5 * MINUTE_IN_SECONDS,
|
|
'display' => __('Every 5 Minutes', 'hvac-community-events')
|
|
);
|
|
|
|
$schedules['every_15_minutes'] = array(
|
|
'interval' => 15 * MINUTE_IN_SECONDS,
|
|
'display' => __('Every 15 Minutes', 'hvac-community-events')
|
|
);
|
|
|
|
$schedules['every_30_minutes'] = array(
|
|
'interval' => 30 * MINUTE_IN_SECONDS,
|
|
'display' => __('Every 30 Minutes', 'hvac-community-events')
|
|
);
|
|
|
|
$schedules['every_6_hours'] = array(
|
|
'interval' => 6 * HOUR_IN_SECONDS,
|
|
'display' => __('Every 6 Hours', 'hvac-community-events')
|
|
);
|
|
|
|
return $schedules;
|
|
}
|
|
|
|
/**
|
|
* Schedule the sync cron event
|
|
*
|
|
* @param string|null $interval Optional interval override
|
|
* @return bool True if scheduled successfully
|
|
*/
|
|
public function schedule_sync($interval = null) {
|
|
// Unschedule any existing event first
|
|
$this->unschedule_sync();
|
|
|
|
// Get interval from option if not provided
|
|
if (null === $interval) {
|
|
$interval = get_option(self::OPTION_INTERVAL, 'every_5_minutes');
|
|
}
|
|
|
|
// Schedule the event
|
|
$result = wp_schedule_event(time(), $interval, self::CRON_HOOK);
|
|
|
|
if ($result !== false) {
|
|
if (class_exists('HVAC_Logger')) {
|
|
HVAC_Logger::info("Scheduled Zoho sync with interval: {$interval}", 'ZohoScheduledSync');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Unschedule the sync cron event
|
|
*
|
|
* @return bool True if unscheduled successfully
|
|
*/
|
|
public function unschedule_sync() {
|
|
$timestamp = wp_next_scheduled(self::CRON_HOOK);
|
|
|
|
if ($timestamp) {
|
|
wp_unschedule_event($timestamp, self::CRON_HOOK);
|
|
|
|
if (class_exists('HVAC_Logger')) {
|
|
HVAC_Logger::info('Unscheduled Zoho sync', 'ZohoScheduledSync');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if sync is currently scheduled
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_scheduled() {
|
|
return wp_next_scheduled(self::CRON_HOOK) !== false;
|
|
}
|
|
|
|
/**
|
|
* Get next scheduled sync time
|
|
*
|
|
* @return int|false Timestamp or false if not scheduled
|
|
*/
|
|
public function get_next_scheduled() {
|
|
return wp_next_scheduled(self::CRON_HOOK);
|
|
}
|
|
|
|
/**
|
|
* Handle first-time option creation
|
|
*
|
|
* @param string $option Option name
|
|
* @param mixed $value Option value
|
|
*/
|
|
public function on_setting_added($option, $value) {
|
|
if ($value === '1' || $value === 1 || $value === true) {
|
|
$this->schedule_sync();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle setting enabled/disabled change
|
|
*
|
|
* @param mixed $old_value Old option value
|
|
* @param mixed $new_value New option value
|
|
*/
|
|
public function on_setting_change($old_value, $new_value) {
|
|
if ($new_value === '1' || $new_value === 1 || $new_value === true) {
|
|
$this->schedule_sync();
|
|
} else {
|
|
$this->unschedule_sync();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle interval change
|
|
*
|
|
* @param mixed $old_value Old interval value
|
|
* @param mixed $new_value New interval value
|
|
*/
|
|
public function on_interval_change($old_value, $new_value) {
|
|
// Only reschedule if currently enabled
|
|
if (get_option(self::OPTION_ENABLED) === '1') {
|
|
$this->schedule_sync($new_value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run the scheduled sync
|
|
*
|
|
* This is the main cron callback that syncs all data types.
|
|
*/
|
|
public function run_scheduled_sync() {
|
|
$start_time = time();
|
|
|
|
if (class_exists('HVAC_Logger')) {
|
|
HVAC_Logger::info('Starting scheduled Zoho sync', 'ZohoScheduledSync');
|
|
}
|
|
|
|
// Get last sync time for incremental sync
|
|
$last_sync = $this->get_last_sync_time();
|
|
|
|
// Initialize results
|
|
$results = array(
|
|
'started_at' => date('Y-m-d H:i:s', $start_time),
|
|
'last_sync_from' => $last_sync ? date('Y-m-d H:i:s', $last_sync) : 'Never',
|
|
'events' => null,
|
|
'users' => null,
|
|
'attendees' => null,
|
|
'rsvps' => null,
|
|
'purchases' => null,
|
|
'errors' => array(),
|
|
);
|
|
|
|
try {
|
|
// Load dependencies
|
|
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-sync.php';
|
|
$sync = new HVAC_Zoho_Sync();
|
|
|
|
// Sync each type with since_timestamp for incremental sync
|
|
// Events
|
|
$results['events'] = $this->sync_all_batches($sync, 'sync_events', $last_sync);
|
|
|
|
// Users/Trainers
|
|
$results['users'] = $this->sync_all_batches($sync, 'sync_users', $last_sync);
|
|
|
|
// Attendees
|
|
$results['attendees'] = $this->sync_all_batches($sync, 'sync_attendees', $last_sync);
|
|
|
|
// RSVPs
|
|
$results['rsvps'] = $this->sync_all_batches($sync, 'sync_rsvps', $last_sync);
|
|
|
|
// Purchases/Orders
|
|
$results['purchases'] = $this->sync_all_batches($sync, 'sync_purchases', $last_sync);
|
|
|
|
} catch (Exception $e) {
|
|
$results['errors'][] = $e->getMessage();
|
|
|
|
if (class_exists('HVAC_Logger')) {
|
|
HVAC_Logger::error('Scheduled sync error: ' . $e->getMessage(), 'ZohoScheduledSync');
|
|
}
|
|
}
|
|
|
|
// Update last sync time
|
|
$this->set_last_sync_time($start_time);
|
|
|
|
// 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'];
|
|
}
|
|
}
|
|
|
|
$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_skipped} skipped, {$total_failed} failed", 'ZohoScheduledSync');
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Sync all batches for a given sync method
|
|
*
|
|
* @param HVAC_Zoho_Sync $sync Sync instance
|
|
* @param string $method Method name (e.g., 'sync_events')
|
|
* @param int|null $since_timestamp Optional timestamp for incremental sync
|
|
* @return array Aggregated results
|
|
*/
|
|
private function sync_all_batches($sync, $method, $since_timestamp = null) {
|
|
$offset = 0;
|
|
$limit = 50;
|
|
$aggregated = array(
|
|
'total' => 0,
|
|
'synced' => 0,
|
|
'skipped' => 0,
|
|
'failed' => 0,
|
|
'errors' => array(),
|
|
);
|
|
|
|
do {
|
|
// Call the sync method with since_timestamp support
|
|
$result = $sync->$method($offset, $limit, $since_timestamp);
|
|
|
|
// 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'])) {
|
|
$aggregated['errors'] = array_merge($aggregated['errors'], $result['errors']);
|
|
}
|
|
|
|
// Check for more batches
|
|
$has_more = $result['has_more'] ?? false;
|
|
$offset = $result['next_offset'] ?? ($offset + $limit);
|
|
|
|
} while ($has_more);
|
|
|
|
return $aggregated;
|
|
}
|
|
|
|
/**
|
|
* Get the last sync timestamp
|
|
*
|
|
* @return int|null Timestamp or null if never synced
|
|
*/
|
|
public function get_last_sync_time() {
|
|
$time = get_option(self::OPTION_LAST_SYNC);
|
|
return $time ? (int) $time : null;
|
|
}
|
|
|
|
/**
|
|
* Set the last sync timestamp
|
|
*
|
|
* @param int $timestamp Timestamp
|
|
*/
|
|
public function set_last_sync_time($timestamp) {
|
|
update_option(self::OPTION_LAST_SYNC, $timestamp);
|
|
}
|
|
|
|
/**
|
|
* Get the last sync result
|
|
*
|
|
* @return array|null Result array or null
|
|
*/
|
|
public function get_last_sync_result() {
|
|
return get_option(self::OPTION_LAST_RESULT);
|
|
}
|
|
|
|
/**
|
|
* Get sync status summary
|
|
*
|
|
* @return array Status information
|
|
*/
|
|
public function get_status() {
|
|
$is_enabled = get_option(self::OPTION_ENABLED) === '1';
|
|
$interval = get_option(self::OPTION_INTERVAL, 'every_5_minutes');
|
|
$last_sync = $this->get_last_sync_time();
|
|
$next_sync = $this->get_next_scheduled();
|
|
$last_result = $this->get_last_sync_result();
|
|
|
|
return array(
|
|
'enabled' => $is_enabled,
|
|
'interval' => $interval,
|
|
'is_scheduled' => $this->is_scheduled(),
|
|
'last_sync_time' => $last_sync,
|
|
'last_sync_formatted' => $last_sync ? date('Y-m-d H:i:s', $last_sync) : 'Never',
|
|
'next_sync_time' => $next_sync,
|
|
'next_sync_formatted' => $next_sync ? date('Y-m-d H:i:s', $next_sync) : 'Not scheduled',
|
|
'last_result' => $last_result,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Run sync manually (for "Run Now" button)
|
|
*
|
|
* @return array Sync results
|
|
*/
|
|
public function run_now() {
|
|
return $this->run_scheduled_sync();
|
|
}
|
|
}
|