Some checks failed
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
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
Security Monitoring & Compliance / Security Summary Report (push) Has been cancelled
Security Monitoring & Compliance / Security Team Notification (push) Has been cancelled
- Load HVAC_Zoho_Scheduled_Sync on ALL requests (not just admin) so WP-Cron can find custom schedules and action hooks - Add add_option hook for first-time setting creation - Explicitly call schedule_sync() in save_settings() to ensure scheduling works even when option value hasn't changed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
389 lines
12 KiB
PHP
389 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_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]['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_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');
|
|
}
|
|
|
|
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,
|
|
'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['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();
|
|
}
|
|
}
|