upskill-event-manager/includes/zoho/class-zoho-scheduled-sync.php
ben 4fc6676e0c
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
fix: Zoho scheduled sync persistence issue
- 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>
2025-12-20 09:06:59 -04:00

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();
}
}