fix: Zoho scheduled sync persistence issue
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>
This commit is contained in:
ben 2025-12-20 09:06:59 -04:00
parent 5a55b78d03
commit 4fc6676e0c
3 changed files with 650 additions and 52 deletions

View file

@ -43,6 +43,8 @@ class HVAC_Zoho_Admin {
add_action('wp_ajax_hvac_zoho_sync_data', array($this, 'sync_data'));
add_action('wp_ajax_hvac_zoho_save_credentials', array($this, 'save_credentials'));
add_action('wp_ajax_hvac_zoho_flush_rewrite_rules', array($this, 'flush_rewrite_rules_ajax'));
add_action('wp_ajax_hvac_zoho_save_settings', array($this, 'save_settings'));
add_action('wp_ajax_hvac_zoho_run_scheduled_sync', array($this, 'run_scheduled_sync_now'));
// Add simple test handler
add_action('wp_ajax_hvac_zoho_simple_test', array($this, 'simple_test'));
// Add OAuth callback handler - only use one method to prevent duplicates
@ -50,8 +52,22 @@ class HVAC_Zoho_Admin {
add_filter('query_vars', array($this, 'add_oauth_query_vars'), 10, 1);
add_action('template_redirect', array($this, 'handle_oauth_template_redirect'));
// Fallback: Check for OAuth params on init (in case rewrite rules fail and we land on homepage)
add_action('init', array($this, 'check_for_oauth_params'));
// Ensure rewrite rules are flushed when plugin is activated
register_activation_hook(HVAC_PLUGIN_FILE, array($this, 'flush_rewrite_rules_on_activation'));
// Initialize scheduled sync if enabled
$this->init_scheduled_sync();
}
/**
* Initialize scheduled sync
*/
private function init_scheduled_sync() {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
HVAC_Zoho_Scheduled_Sync::instance();
}
/**
@ -125,27 +141,13 @@ class HVAC_Zoho_Admin {
// Debug logging
// More robust production detection
$parsed_url = parse_url($site_url);
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
// Remove www prefix for comparison
$clean_host = preg_replace('/^www\./', '', $host);
// Check if this is production
$is_production = ($clean_host === 'upskillhvac.com');
// Double-check with string comparison as fallback
if (!$is_production) {
$is_production = (strpos($site_url, 'upskillhvac.com') !== false &&
strpos($site_url, 'staging') === false &&
strpos($site_url, 'test') === false &&
strpos($site_url, 'dev') === false &&
strpos($site_url, 'cloudwaysapps.com') === false);
// Ensure Auth class is loaded for staging detection
if (!class_exists('HVAC_Zoho_CRM_Auth')) {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-crm-auth.php';
}
// Set staging as opposite of production
$is_staging = !$is_production;
// Use central logic for staging detection
$is_staging = HVAC_Zoho_CRM_Auth::is_staging_mode();
// Load secure storage class
@ -268,6 +270,9 @@ class HVAC_Zoho_Admin {
🚀 Authorize with Zoho
</button>
<?php endif; ?>
<button type="button" class="button button-secondary" id="diagnostic-test" style="margin-left: 10px;">
🏥 Run Diagnostic Test
</button>
</p>
</form>
</div>
@ -329,24 +334,82 @@ class HVAC_Zoho_Admin {
</div>
<div class="hvac-zoho-settings">
<h2>Sync Settings</h2>
<h2>Scheduled Sync Settings</h2>
<?php
// Get scheduled sync status
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
$sync_status = $scheduled_sync->get_status();
$current_frequency = get_option('hvac_zoho_sync_frequency', 'every_5_minutes');
?>
<form id="zoho-settings-form">
<label>
<input type="checkbox" name="auto_sync" value="1" <?php checked(get_option('hvac_zoho_auto_sync'), '1'); ?>>
Enable automatic sync
</label>
<br><br>
<label>
Sync frequency:
<select name="sync_frequency">
<option value="hourly" <?php selected(get_option('hvac_zoho_sync_frequency'), 'hourly'); ?>>Hourly</option>
<option value="daily" <?php selected(get_option('hvac_zoho_sync_frequency'), 'daily'); ?>>Daily</option>
<option value="weekly" <?php selected(get_option('hvac_zoho_sync_frequency'), 'weekly'); ?>>Weekly</option>
</select>
</label>
<br><br>
<button type="submit" class="button button-primary">Save Settings</button>
<table class="form-table">
<tr>
<th scope="row">Enable Scheduled Sync</th>
<td>
<label>
<input type="checkbox" name="auto_sync" value="1" <?php checked(get_option('hvac_zoho_auto_sync'), '1'); ?>>
Automatically sync new/modified records to Zoho CRM
</label>
<p class="description">When enabled, a background process will sync changes on the selected interval.</p>
</td>
</tr>
<tr>
<th scope="row">Sync Interval</th>
<td>
<select name="sync_frequency">
<option value="every_5_minutes" <?php selected($current_frequency, 'every_5_minutes'); ?>>Every 5 minutes</option>
<option value="every_15_minutes" <?php selected($current_frequency, 'every_15_minutes'); ?>>Every 15 minutes</option>
<option value="every_30_minutes" <?php selected($current_frequency, 'every_30_minutes'); ?>>Every 30 minutes</option>
<option value="hourly" <?php selected($current_frequency, 'hourly'); ?>>Hourly</option>
<option value="every_6_hours" <?php selected($current_frequency, 'every_6_hours'); ?>>Every 6 hours</option>
<option value="daily" <?php selected($current_frequency, 'daily'); ?>>Daily</option>
</select>
<p class="description">How often to check for and sync new/modified records.</p>
</td>
</tr>
<tr>
<th scope="row">Sync Status</th>
<td>
<p>
<strong>Status:</strong>
<?php if ($sync_status['is_scheduled']): ?>
<span style="color: #46b450;"> Scheduled</span>
<?php else: ?>
<span style="color: #dc3232;"> Not Scheduled</span>
<?php endif; ?>
</p>
<p>
<strong>Last Sync:</strong>
<?php echo esc_html($sync_status['last_sync_formatted']); ?>
</p>
<?php if ($sync_status['is_scheduled']): ?>
<p>
<strong>Next Sync:</strong>
<?php echo esc_html($sync_status['next_sync_formatted']); ?>
</p>
<?php endif; ?>
<?php if ($sync_status['last_result']): ?>
<p>
<strong>Last Result:</strong>
<?php
$last = $sync_status['last_result'];
echo esc_html(sprintf('%d synced, %d failed', $last['total_synced'] ?? 0, $last['total_failed'] ?? 0));
?>
</p>
<?php endif; ?>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary">Save Settings</button>
<button type="button" class="button button-secondary" id="run-scheduled-sync-now" style="margin-left: 10px;">
🔄 Run Sync Now
</button>
</p>
</form>
<div id="scheduled-sync-status"></div>
</div>
<?php endif; ?>
</div>
@ -359,8 +422,23 @@ class HVAC_Zoho_Admin {
* Simple test handler to isolate issues
*/
public function simple_test() {
// Check permissions
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Unauthorized access'));
return;
}
wp_send_json_success(array('message' => 'Simple test works!'));
$payload_status = 'No payload received';
if (!empty($_POST['test_payload'])) {
$payload_status = 'Payload received (' . strlen($_POST['test_payload']) . ' chars)';
}
wp_send_json_success(array(
'message' => 'Simple test works!',
'server_time' => date('Y-m-d H:i:s'),
'payload_status' => $payload_status,
'request_method' => $_SERVER['REQUEST_METHOD']
));
}
/**
@ -488,9 +566,6 @@ class HVAC_Zoho_Admin {
}
}
/**
* Parse OAuth request using parse_request hook
*/
public function parse_oauth_request($wp) {
// Check if this is an OAuth callback request
@ -507,6 +582,33 @@ class HVAC_Zoho_Admin {
}
}
/**
* Manual Router for OAuth Callback
*
* Catches the request on 'init' before WordPress internal routing can 404 it.
* This is a robust fallback for when rewrite rules fail or haven't flushed.
*/
public function check_for_oauth_params() {
// Check if we are at the oauth callback URL path
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Check strict path match or if regex matches
if (strpos($path, '/oauth/callback') !== false) {
// We are at the right URL. Do we have the code?
if (isset($_GET['code'])) {
// We have a code.
// We MUST process this, otherwise WP will display a 404.
// Even if state matches or not, we should handle it here.
// The process_oauth_callback method handles validation.
$this->process_oauth_callback();
// process_oauth_callback exits, so we won't continue to 404.
}
}
}
/**
* Add OAuth callback rewrite rule
*/
@ -794,10 +896,14 @@ class HVAC_Zoho_Admin {
return;
}
// Success!
// Success!
wp_send_json_success(array(
'message' => 'Connection successful!',
'modules' => isset($response['modules']) ? count($response['modules']) . ' modules available' : 'API connected',
'client_id' => substr($client_id, 0, 10) . '...',
'client_secret_exists' => true,
'refresh_token_exists' => true,
'credentials_status' => array(
'client_id' => substr($client_id, 0, 10) . '...',
'client_secret_exists' => true,
@ -847,6 +953,8 @@ class HVAC_Zoho_Admin {
}
$type = sanitize_text_field($_POST['type']);
$offset = isset($_POST['offset']) ? absint($_POST['offset']) : 0;
$limit = isset($_POST['limit']) ? absint($_POST['limit']) : 50;
try {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-sync.php';
@ -854,19 +962,19 @@ class HVAC_Zoho_Admin {
switch ($type) {
case 'events':
$result = $sync->sync_events();
$result = $sync->sync_events($offset, $limit);
break;
case 'users':
$result = $sync->sync_users();
$result = $sync->sync_users($offset, $limit);
break;
case 'attendees':
$result = $sync->sync_attendees();
$result = $sync->sync_attendees($offset, $limit);
break;
case 'rsvps':
$result = $sync->sync_rsvps();
$result = $sync->sync_rsvps($offset, $limit);
break;
case 'purchases':
$result = $sync->sync_purchases();
$result = $sync->sync_purchases($offset, $limit);
break;
default:
throw new Exception('Invalid sync type');
@ -880,5 +988,89 @@ class HVAC_Zoho_Admin {
));
}
}
/**
* Save scheduled sync settings
*/
public function save_settings() {
check_ajax_referer('hvac_zoho_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Unauthorized access'));
return;
}
$auto_sync = isset($_POST['auto_sync']) && $_POST['auto_sync'] === '1' ? '1' : '0';
$sync_frequency = sanitize_text_field($_POST['sync_frequency'] ?? 'every_5_minutes');
// Validate frequency value
$valid_frequencies = array(
'every_5_minutes',
'every_15_minutes',
'every_30_minutes',
'hourly',
'every_6_hours',
'daily'
);
if (!in_array($sync_frequency, $valid_frequencies)) {
$sync_frequency = 'every_5_minutes';
}
// Save settings
update_option('hvac_zoho_auto_sync', $auto_sync);
update_option('hvac_zoho_sync_frequency', $sync_frequency);
// Get scheduled sync instance and explicitly schedule/unschedule
// This ensures scheduling works even if option value didn't change
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
if ($auto_sync === '1') {
$scheduled_sync->schedule_sync($sync_frequency);
} else {
$scheduled_sync->unschedule_sync();
}
$status = $scheduled_sync->get_status();
wp_send_json_success(array(
'message' => 'Settings saved successfully',
'auto_sync' => $auto_sync,
'sync_frequency' => $sync_frequency,
'is_scheduled' => $status['is_scheduled'],
'next_sync' => $status['next_sync_formatted']
));
}
/**
* Run scheduled sync manually
*/
public function run_scheduled_sync_now() {
check_ajax_referer('hvac_zoho_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Unauthorized access'));
return;
}
try {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
$result = $scheduled_sync->run_now();
wp_send_json_success(array(
'message' => 'Scheduled sync completed',
'result' => $result
));
} catch (Exception $e) {
wp_send_json_error(array(
'message' => 'Sync failed',
'error' => $e->getMessage()
));
}
}
}
?>

View file

@ -337,8 +337,11 @@ final class HVAC_Plugin {
'admin/class-hvac-enhanced-settings.php',
];
// Check if this is an OAuth callback request
$is_oauth_callback = isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/oauth/callback') !== false;
// Load admin files conditionally
if (is_admin() || wp_doing_ajax()) {
if (is_admin() || wp_doing_ajax() || $is_oauth_callback) {
foreach ($this->loadFeatureFiles($adminFiles) as $file => $status) {
if ($status === 'loaded') {
$this->componentStatus["admin_{$file}"] = true;
@ -548,8 +551,10 @@ final class HVAC_Plugin {
// Schedule non-critical components for lazy loading
// Use 'init' instead of 'wp_loaded' so components can register wp_enqueue_scripts hooks
add_action('init', [$this, 'initializeSecondaryComponents'], 5);
// Use admin_menu (not admin_init) so components can register their menus in time
add_action('admin_menu', [$this, 'initializeAdminComponents'], 5);
// Use 'init' for admin components too, ensuring AJAX handlers are registered
// (admin_menu hook doesn't fire on AJAX requests, causing 400 Bad Request/0 response)
add_action('init', [$this, 'initializeAdminComponents'], 5);
}
/**
@ -702,6 +707,15 @@ final class HVAC_Plugin {
hvac_communication_scheduler();
}
// Initialize Zoho scheduled sync (must load on ALL requests for WP-Cron to work)
// This registers custom cron schedules and the cron action hook
if (file_exists(HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php')) {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
if (class_exists('HVAC_Zoho_Scheduled_Sync')) {
HVAC_Zoho_Scheduled_Sync::instance();
}
}
// Initialize Master Trainer manager classes (fix for missing shortcode registrations)
if (class_exists('HVAC_Master_Events_Overview')) {
HVAC_Master_Events_Overview::instance();
@ -731,19 +745,22 @@ final class HVAC_Plugin {
* Only loads admin components in admin context to improve frontend performance.
*/
public function initializeAdminComponents(): void {
// Check if this is an OAuth callback request
$is_oauth_callback = isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/oauth/callback') !== false;
// Initialize admin components only when needed
if (class_exists('HVAC_Zoho_Admin')) {
if (class_exists('HVAC_Zoho_Admin') && (is_admin() || wp_doing_ajax() || $is_oauth_callback)) {
HVAC_Zoho_Admin::instance();
}
if (class_exists('HVAC_Admin_Dashboard')) {
if (class_exists('HVAC_Admin_Dashboard') && is_admin()) {
new HVAC_Admin_Dashboard();
}
if (class_exists('HVAC_Enhanced_Settings')) {
if (class_exists('HVAC_Enhanced_Settings') && is_admin()) {
HVAC_Enhanced_Settings::instance();
}
// Initialize trainer certification admin interface
if (class_exists('HVAC_Certification_Admin') && current_user_can('manage_hvac_certifications')) {
if (class_exists('HVAC_Certification_Admin') && current_user_can('manage_hvac_certifications') && is_admin()) {
HVAC_Certification_Admin::instance();
}
}

View file

@ -0,0 +1,389 @@
<?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();
}
}