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.
1406 lines
No EOL
56 KiB
PHP
1406 lines
No EOL
56 KiB
PHP
<?php
|
|
/**
|
|
* Zoho CRM Sync Handler
|
|
*
|
|
* @package HVACCommunityEvents
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Zoho Sync Class
|
|
*/
|
|
class HVAC_Zoho_Sync {
|
|
|
|
/**
|
|
* Zoho Auth instance
|
|
*
|
|
* @var HVAC_Zoho_CRM_Auth
|
|
*/
|
|
private $auth;
|
|
|
|
/**
|
|
* Staging mode flag
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $is_staging;
|
|
|
|
/**
|
|
* Last contact creation error for debugging
|
|
*
|
|
* @var string
|
|
*/
|
|
private $last_contact_error = '';
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-crm-auth.php';
|
|
$this->auth = new HVAC_Zoho_CRM_Auth();
|
|
|
|
// Determine if we're in staging mode
|
|
$this->is_staging = HVAC_Zoho_CRM_Auth::is_staging_mode();
|
|
}
|
|
|
|
/**
|
|
* Check if sync is allowed
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_sync_allowed() {
|
|
// Use consistent logic from Auth class
|
|
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
|
|
*
|
|
* @param int $offset Starting offset for pagination
|
|
* @param int $limit Number of records per batch
|
|
* @param int|null $since_timestamp Optional timestamp to filter modified records
|
|
* @return array Sync results
|
|
*/
|
|
public function sync_events($offset = 0, $limit = 50, $since_timestamp = null) {
|
|
$results = array(
|
|
'total' => 0,
|
|
'synced' => 0,
|
|
'skipped' => 0,
|
|
'failed' => 0,
|
|
'errors' => array(),
|
|
'staging_mode' => $this->is_staging,
|
|
'debug_info' => HVAC_Zoho_CRM_Auth::get_debug_mode_info(),
|
|
'has_more' => false,
|
|
'next_offset' => 0,
|
|
'batch_offset' => $offset,
|
|
'incremental_sync' => !is_null($since_timestamp)
|
|
);
|
|
|
|
// Build base query args
|
|
$base_event_args = array(
|
|
'posts_per_page' => -1,
|
|
'eventDisplay' => 'custom',
|
|
'start_date' => '2020-01-01',
|
|
);
|
|
|
|
$base_series_args = array(
|
|
'post_type' => 'tribe_event_series',
|
|
'posts_per_page' => -1,
|
|
'post_status' => 'publish',
|
|
);
|
|
|
|
// Add date filter for incremental sync
|
|
if ($since_timestamp) {
|
|
$since_date = date('Y-m-d H:i:s', $since_timestamp);
|
|
$date_query = array(
|
|
array(
|
|
'column' => 'post_modified',
|
|
'after' => $since_date,
|
|
'inclusive' => true,
|
|
)
|
|
);
|
|
$base_event_args['date_query'] = $date_query;
|
|
$base_series_args['date_query'] = $date_query;
|
|
}
|
|
|
|
// First, get total count
|
|
$all_events = tribe_get_events($base_event_args);
|
|
$event_series = get_posts($base_series_args);
|
|
$total_count = count($all_events) + count($event_series);
|
|
$results['total'] = $total_count;
|
|
|
|
// Get paginated events
|
|
$paginated_args = array_merge($base_event_args, array(
|
|
'posts_per_page' => $limit,
|
|
'offset' => $offset,
|
|
));
|
|
$events = tribe_get_events($paginated_args);
|
|
|
|
// Also get event series (paginated based on remaining limit)
|
|
$remaining = max(0, $limit - count($events));
|
|
$series_offset = max(0, $offset - count($all_events));
|
|
if ($remaining > 0 && $series_offset >= 0) {
|
|
$event_series_batch = get_posts(array(
|
|
'post_type' => 'tribe_event_series',
|
|
'posts_per_page' => $remaining,
|
|
'offset' => $series_offset,
|
|
'post_status' => 'publish',
|
|
));
|
|
$events = array_merge($events, $event_series_batch);
|
|
}
|
|
|
|
// Calculate pagination
|
|
$results['has_more'] = ($offset + count($events)) < $total_count;
|
|
$results['next_offset'] = $offset + count($events);
|
|
|
|
// If staging mode, simulate the sync
|
|
if ($this->is_staging) {
|
|
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
|
|
$results['synced'] = $results['total'];
|
|
$results['test_data'] = array();
|
|
|
|
foreach ($events as $event) {
|
|
$campaign_data = $this->prepare_campaign_data($event);
|
|
$results['test_data'][] = array(
|
|
'event_id' => $event->ID,
|
|
'event_title' => get_the_title($event),
|
|
'zoho_data' => $campaign_data
|
|
);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
// Production sync
|
|
if (!$this->is_sync_allowed()) {
|
|
$results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.';
|
|
return $results;
|
|
}
|
|
|
|
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
|
|
$stored_campaign_id = get_post_meta($event->ID, '_zoho_campaign_id', true);
|
|
|
|
if (!empty($stored_campaign_id)) {
|
|
// We have a stored ID - try to update
|
|
$update_response = $this->auth->make_api_request("/Campaigns/{$stored_campaign_id}", 'PUT', array(
|
|
'data' => array($campaign_data)
|
|
));
|
|
|
|
// Check if update failed due to invalid ID (e.g. campaign deleted in Zoho)
|
|
if (isset($update_response['code']) && $update_response['code'] === 'INVALID_DATA') {
|
|
// Fallback: Create new campaign
|
|
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
|
|
'data' => array($campaign_data)
|
|
));
|
|
$results['responses'][] = array('type' => 'create_fallback', 'id' => $event->ID, 'response' => $create_response);
|
|
|
|
if (!empty($create_response['data'][0]['details']['id'])) {
|
|
$campaign_id = $create_response['data'][0]['details']['id'];
|
|
}
|
|
} else {
|
|
$campaign_id = $stored_campaign_id;
|
|
$results['responses'][] = array('type' => 'update', 'id' => $event->ID, 'response' => $update_response);
|
|
}
|
|
} else {
|
|
// No stored ID - create new campaign
|
|
$create_response = $this->auth->make_api_request('/Campaigns', 'POST', array(
|
|
'data' => array($campaign_data)
|
|
));
|
|
$results['responses'][] = array('type' => 'create', 'id' => $event->ID, 'response' => $create_response);
|
|
|
|
// Extract campaign ID from create response
|
|
if (!empty($create_response['data'][0]['details']['id'])) {
|
|
$campaign_id = $create_response['data'][0]['details']['id'];
|
|
}
|
|
}
|
|
|
|
$results['synced']++;
|
|
|
|
// 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']++;
|
|
$results['errors'][] = sprintf('Event %s: %s', $event->ID, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Sync users to Zoho Contacts
|
|
*
|
|
* @param int $offset Starting offset for pagination
|
|
* @param int $limit Number of records per batch
|
|
* @param int|null $since_timestamp Optional timestamp to filter modified records
|
|
* @return array Sync results
|
|
*/
|
|
public function sync_users($offset = 0, $limit = 50, $since_timestamp = null) {
|
|
$results = array(
|
|
'total' => 0,
|
|
'synced' => 0,
|
|
'skipped' => 0,
|
|
'failed' => 0,
|
|
'errors' => array(),
|
|
'staging_mode' => $this->is_staging,
|
|
'has_more' => false,
|
|
'next_offset' => 0,
|
|
'batch_offset' => $offset,
|
|
'incremental_sync' => !is_null($since_timestamp)
|
|
);
|
|
|
|
// Base query args
|
|
$base_args = array(
|
|
'role__in' => array('hvac_trainer', 'hvac_master_trainer'),
|
|
'meta_query' => array(
|
|
'relation' => 'OR',
|
|
array(
|
|
'key' => '_sync_to_zoho',
|
|
'value' => '1',
|
|
'compare' => '='
|
|
),
|
|
array(
|
|
'key' => '_sync_to_zoho',
|
|
'compare' => 'NOT EXISTS'
|
|
)
|
|
)
|
|
);
|
|
|
|
// Note: WP_User_Query doesn't support date_query for user_registered
|
|
// For users, we'll sync all and let Zoho handle updates
|
|
// Future enhancement: track _zoho_last_synced per user
|
|
|
|
// Get total count first
|
|
$total_query = new WP_User_Query(array_merge($base_args, array('count_total' => true, 'number' => -1)));
|
|
$total_count = $total_query->get_total();
|
|
$results['total'] = $total_count;
|
|
|
|
// Get paginated users
|
|
$users = get_users(array_merge($base_args, array(
|
|
'number' => $limit,
|
|
'offset' => $offset
|
|
)));
|
|
|
|
// Calculate pagination
|
|
$results['has_more'] = ($offset + count($users)) < $total_count;
|
|
$results['next_offset'] = $offset + count($users);
|
|
|
|
// If staging mode, simulate the sync
|
|
if ($this->is_staging) {
|
|
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
|
|
$results['synced'] = $results['total'];
|
|
$results['test_data'] = array();
|
|
|
|
foreach ($users as $user) {
|
|
$contact_data = $this->prepare_contact_data($user);
|
|
$results['test_data'][] = array(
|
|
'user_id' => $user->ID,
|
|
'user_email' => $user->user_email,
|
|
'user_role' => implode(', ', $user->roles),
|
|
'zoho_data' => $contact_data
|
|
);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
// Production sync
|
|
if (!$this->is_sync_allowed()) {
|
|
$results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.';
|
|
return $results;
|
|
}
|
|
|
|
foreach ($users as $user) {
|
|
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']})"
|
|
));
|
|
|
|
if (!empty($search_response['data'])) {
|
|
// Update existing contact
|
|
$contact_id = $search_response['data'][0]['id'];
|
|
$update_response = $this->auth->make_api_request("/Contacts/{$contact_id}", 'PUT', array(
|
|
'data' => array($contact_data)
|
|
));
|
|
} else {
|
|
// Create new contact
|
|
$create_response = $this->auth->make_api_request('/Contacts', 'POST', array(
|
|
'data' => array($contact_data)
|
|
));
|
|
|
|
if (!empty($create_response['data'][0]['details']['id'])) {
|
|
$contact_id = $create_response['data'][0]['details']['id'];
|
|
}
|
|
}
|
|
|
|
$results['synced']++;
|
|
|
|
// 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']++;
|
|
$results['errors'][] = sprintf('User %s: %s', $user->ID, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Sync Event Tickets orders to Zoho Invoices
|
|
*
|
|
* @param int $offset Starting offset for pagination
|
|
* @param int $limit Number of records per batch
|
|
* @param int|null $since_timestamp Optional timestamp to filter modified records
|
|
* @return array Sync results
|
|
*/
|
|
public function sync_purchases($offset = 0, $limit = 50, $since_timestamp = null) {
|
|
$results = array(
|
|
'total' => 0,
|
|
'synced' => 0,
|
|
'skipped' => 0,
|
|
'failed' => 0,
|
|
'errors' => array(),
|
|
'staging_mode' => $this->is_staging,
|
|
'has_more' => false,
|
|
'next_offset' => 0,
|
|
'batch_offset' => $offset,
|
|
'incremental_sync' => !is_null($since_timestamp)
|
|
);
|
|
|
|
// Build query args
|
|
$query_args = array(
|
|
'post_type' => 'tec_tc_order',
|
|
'post_status' => 'tec-tc-completed',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids'
|
|
);
|
|
|
|
// Add date filter for incremental sync
|
|
if ($since_timestamp) {
|
|
$query_args['date_query'] = array(
|
|
array(
|
|
'column' => 'post_modified',
|
|
'after' => date('Y-m-d H:i:s', $since_timestamp),
|
|
'inclusive' => true,
|
|
)
|
|
);
|
|
}
|
|
|
|
// Get total count first
|
|
$count_query = new WP_Query($query_args);
|
|
$total_count = $count_query->found_posts;
|
|
$results['total'] = $total_count;
|
|
|
|
if ($total_count === 0) {
|
|
$results['message'] = 'No completed ticket orders found.';
|
|
return $results;
|
|
}
|
|
|
|
// Get paginated orders
|
|
$paginated_args = array_merge($query_args, array(
|
|
'posts_per_page' => $limit,
|
|
'offset' => $offset,
|
|
'fields' => 'all',
|
|
));
|
|
$orders = get_posts($paginated_args);
|
|
|
|
// Calculate pagination
|
|
$results['has_more'] = ($offset + count($orders)) < $total_count;
|
|
$results['next_offset'] = $offset + count($orders);
|
|
|
|
// If staging mode, simulate the sync
|
|
if ($this->is_staging) {
|
|
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
|
|
$results['synced'] = $results['total'];
|
|
$results['test_data'] = array();
|
|
|
|
foreach ($orders as $order) {
|
|
$invoice_data = $this->prepare_tc_invoice_data($order);
|
|
$results['test_data'][] = array(
|
|
'order_id' => $order->ID,
|
|
'purchaser_email' => get_post_meta($order->ID, '_tec_tc_order_purchaser_email', true),
|
|
'gateway' => get_post_meta($order->ID, '_tec_tc_order_gateway', true),
|
|
'date' => $order->post_date,
|
|
'zoho_data' => $invoice_data
|
|
);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
// Production sync
|
|
if (!$this->is_sync_allowed()) {
|
|
$results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.';
|
|
return $results;
|
|
}
|
|
|
|
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(
|
|
'/Invoices/search?criteria=(WordPress_Order_ID:equals:' . $order->ID . ')',
|
|
'GET'
|
|
);
|
|
|
|
if (!empty($search_response['data'])) {
|
|
// Update existing invoice
|
|
$invoice_id = $search_response['data'][0]['id'];
|
|
$this->auth->make_api_request("/Invoices/{$invoice_id}", 'PUT', array(
|
|
'data' => array($invoice_data)
|
|
));
|
|
} else {
|
|
// Create new invoice
|
|
$create_response = $this->auth->make_api_request('/Invoices', 'POST', array(
|
|
'data' => array($invoice_data)
|
|
));
|
|
|
|
if (!empty($create_response['data'][0]['details']['id'])) {
|
|
$invoice_id = $create_response['data'][0]['details']['id'];
|
|
}
|
|
}
|
|
|
|
$results['synced']++;
|
|
|
|
// 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']++;
|
|
$results['errors'][] = sprintf('Order %s: %s', $order->ID, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Sync ticket attendees to Zoho Contacts + Campaign Members
|
|
*
|
|
* @param int $offset Starting offset for pagination
|
|
* @param int $limit Number of records per batch
|
|
* @param int|null $since_timestamp Optional timestamp to filter modified records
|
|
* @return array Sync results
|
|
*/
|
|
public function sync_attendees($offset = 0, $limit = 50, $since_timestamp = null) {
|
|
$results = array(
|
|
'total' => 0,
|
|
'synced' => 0,
|
|
'skipped' => 0,
|
|
'failed' => 0,
|
|
'errors' => array(),
|
|
'staging_mode' => $this->is_staging,
|
|
'contacts_created' => 0,
|
|
'campaign_members_created' => 0,
|
|
'responses' => array(),
|
|
'debug_info' => HVAC_Zoho_CRM_Auth::get_debug_mode_info(),
|
|
'version' => 'BATCH_V1',
|
|
'has_more' => false,
|
|
'next_offset' => 0,
|
|
'batch_offset' => $offset,
|
|
'incremental_sync' => !is_null($since_timestamp)
|
|
);
|
|
|
|
// Build base query args
|
|
$base_tc_args = array(
|
|
'post_type' => 'tec_tc_attendee',
|
|
'post_status' => 'any',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids'
|
|
);
|
|
$base_tpp_args = array(
|
|
'post_type' => 'tribe_tpp_attendees',
|
|
'post_status' => 'any',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids'
|
|
);
|
|
|
|
// Add date filter for incremental sync
|
|
if ($since_timestamp) {
|
|
$date_query = array(
|
|
array(
|
|
'column' => 'post_date',
|
|
'after' => date('Y-m-d H:i:s', $since_timestamp),
|
|
'inclusive' => true,
|
|
)
|
|
);
|
|
$base_tc_args['date_query'] = $date_query;
|
|
$base_tpp_args['date_query'] = $date_query;
|
|
}
|
|
|
|
// Get total count for all attendee types
|
|
$tc_count_query = new WP_Query($base_tc_args);
|
|
$tpp_count_query = new WP_Query($base_tpp_args);
|
|
$total_count = $tc_count_query->found_posts + $tpp_count_query->found_posts;
|
|
$results['total'] = $total_count;
|
|
|
|
if ($total_count === 0) {
|
|
$results['message'] = 'No ticket attendees found.';
|
|
return $results;
|
|
}
|
|
|
|
// Get paginated attendees - first from tec_tc_attendee
|
|
$tc_paginated_args = array_merge($base_tc_args, array(
|
|
'posts_per_page' => $limit,
|
|
'offset' => $offset,
|
|
'fields' => 'all',
|
|
));
|
|
$tc_attendees = get_posts($tc_paginated_args);
|
|
|
|
// If we need more to fill the batch, get from PayPal attendees
|
|
$remaining = max(0, $limit - count($tc_attendees));
|
|
$tpp_offset = max(0, $offset - $tc_count_query->found_posts);
|
|
$tpp_attendees = array();
|
|
if ($remaining > 0 && $tpp_offset >= 0) {
|
|
$tpp_paginated_args = array_merge($base_tpp_args, array(
|
|
'posts_per_page' => $remaining,
|
|
'offset' => $tpp_offset,
|
|
'fields' => 'all',
|
|
));
|
|
$tpp_attendees = get_posts($tpp_paginated_args);
|
|
}
|
|
|
|
$all_attendees = array_merge($tc_attendees, $tpp_attendees);
|
|
|
|
// Calculate pagination
|
|
$results['has_more'] = ($offset + count($all_attendees)) < $total_count;
|
|
$results['next_offset'] = $offset + count($all_attendees);
|
|
|
|
if (count($all_attendees) === 0) {
|
|
$results['message'] = 'No ticket attendees found.';
|
|
return $results;
|
|
}
|
|
|
|
// If staging mode, simulate the sync
|
|
if ($this->is_staging) {
|
|
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
|
|
$results['synced'] = $results['total'];
|
|
$results['test_data'] = array();
|
|
|
|
foreach ($all_attendees as $attendee) {
|
|
$attendee_data = $this->prepare_attendee_data($attendee);
|
|
$results['test_data'][] = $attendee_data;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
// Production sync
|
|
if (!$this->is_sync_allowed()) {
|
|
$results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.';
|
|
return $results;
|
|
}
|
|
|
|
$results['version'] = 'PRODUCTION_BATCH_V1';
|
|
|
|
// Wrap entire loop in try-catch for safety (catches PHP 7+ errors)
|
|
try {
|
|
foreach ($all_attendees as $attendee) {
|
|
|
|
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);
|
|
continue;
|
|
}
|
|
|
|
// Step 1: Create/Update Contact
|
|
$contact_id = $this->ensure_contact_exists($attendee_data);
|
|
if ($contact_id) {
|
|
$results['contacts_created']++;
|
|
}
|
|
|
|
if (count($results['responses']) < 5) {
|
|
$cid_debug = $contact_id ? $contact_id : 'NULL';
|
|
$results['responses'][] = array('type' => 'debug', 'msg' => "Debug: Attendee {$attendee->ID} found Contact ID: " . $cid_debug);
|
|
}
|
|
|
|
// Step 2: Create Campaign Member (link Contact to Campaign)
|
|
if ($contact_id && !empty($attendee_data['event_id'])) {
|
|
$campaign_id = get_post_meta($attendee_data['event_id'], '_zoho_campaign_id', true);
|
|
|
|
// Debug: Log event_id and campaign_id for troubleshooting
|
|
if (count($results['responses']) < 10) {
|
|
$results['responses'][] = array(
|
|
'type' => 'campaign_lookup',
|
|
'attendee_id' => $attendee->ID,
|
|
'event_id' => $attendee_data['event_id'],
|
|
'campaign_id' => $campaign_id ?: 'NOT_SET'
|
|
);
|
|
}
|
|
|
|
if ($campaign_id) {
|
|
$assoc_response = $this->create_campaign_member($contact_id, $campaign_id, 'Attended');
|
|
|
|
// ALWAYS capture the first link attempt for debugging
|
|
if (!isset($results['first_link_attempt'])) {
|
|
$results['first_link_attempt'] = array(
|
|
'attendee_id' => $attendee->ID,
|
|
'contact_id' => $contact_id,
|
|
'campaign_id' => $campaign_id,
|
|
'response' => $assoc_response
|
|
);
|
|
}
|
|
|
|
// Debug: Add responses (increased limit to 20)
|
|
if (count($results['responses']) < 20) {
|
|
$results['responses'][] = array('type' => 'link_attempt', 'id' => $attendee->ID, 'response' => $assoc_response);
|
|
}
|
|
|
|
if (isset($assoc_response['status']) && $assoc_response['status'] === 'success') {
|
|
$results['campaign_members_created']++;
|
|
} elseif (isset($assoc_response['data'][0]['status']) && $assoc_response['data'][0]['status'] === 'success') {
|
|
$results['campaign_members_created']++;
|
|
} else {
|
|
$results['errors'][] = sprintf('Attendee %s: Failed to link to campaign. Response: %s', $attendee->ID, json_encode($assoc_response));
|
|
if (isset($assoc_response['data'])) {
|
|
$results['responses'][] = array('type' => 'link_error', 'id' => $attendee->ID, 'response' => $assoc_response);
|
|
}
|
|
}
|
|
} else {
|
|
$results['errors'][] = sprintf('Attendee %s: Event %s has no Zoho Campaign ID', $attendee->ID, $attendee_data['event_id']);
|
|
}
|
|
} elseif (!$contact_id) {
|
|
$error_detail = $this->last_contact_error ?: 'Unknown error';
|
|
$results['errors'][] = sprintf('Attendee %s: No contact_id created. %s', $attendee->ID, $error_detail);
|
|
} elseif (empty($attendee_data['event_id'])) {
|
|
// Debug: Log when event_id is missing
|
|
if (count($results['responses']) < 10) {
|
|
$results['responses'][] = array('type' => 'missing_event_id', 'attendee_id' => $attendee->ID);
|
|
}
|
|
}
|
|
|
|
$results['synced']++;
|
|
|
|
// 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+)
|
|
$results['failed']++;
|
|
$results['errors'][] = sprintf('Attendee %s: [%s] %s', $attendee->ID, get_class($e), $e->getMessage());
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Outer catch for any loop-level errors
|
|
$results['errors'][] = 'Loop error: [' . get_class($e) . '] ' . $e->getMessage();
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Sync RSVPs to Zoho Leads + Campaign Members
|
|
*
|
|
* @param int $offset Starting offset for pagination
|
|
* @param int $limit Number of records per batch
|
|
* @param int|null $since_timestamp Optional timestamp to filter modified records
|
|
* @return array Sync results
|
|
*/
|
|
public function sync_rsvps($offset = 0, $limit = 50, $since_timestamp = null) {
|
|
$results = array(
|
|
'total' => 0,
|
|
'synced' => 0,
|
|
'skipped' => 0,
|
|
'failed' => 0,
|
|
'errors' => array(),
|
|
'staging_mode' => $this->is_staging,
|
|
'leads_created' => 0,
|
|
'campaign_members_created' => 0,
|
|
'debug_info' => HVAC_Zoho_CRM_Auth::get_debug_mode_info(),
|
|
'has_more' => false,
|
|
'next_offset' => 0,
|
|
'batch_offset' => $offset,
|
|
'incremental_sync' => !is_null($since_timestamp)
|
|
);
|
|
|
|
// Build query args
|
|
$query_args = array(
|
|
'post_type' => 'tribe_rsvp_attendees',
|
|
'post_status' => 'any',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids'
|
|
);
|
|
|
|
// Add date filter for incremental sync
|
|
if ($since_timestamp) {
|
|
$query_args['date_query'] = array(
|
|
array(
|
|
'column' => 'post_date',
|
|
'after' => date('Y-m-d H:i:s', $since_timestamp),
|
|
'inclusive' => true,
|
|
)
|
|
);
|
|
}
|
|
|
|
// Get total count first
|
|
$count_query = new WP_Query($query_args);
|
|
$total_count = $count_query->found_posts;
|
|
$results['total'] = $total_count;
|
|
|
|
if ($total_count === 0) {
|
|
$results['message'] = 'No RSVP attendees found.';
|
|
return $results;
|
|
}
|
|
|
|
// Get paginated RSVPs
|
|
$paginated_args = array_merge($query_args, array(
|
|
'posts_per_page' => $limit,
|
|
'offset' => $offset,
|
|
'fields' => 'all',
|
|
));
|
|
$rsvps = get_posts($paginated_args);
|
|
|
|
// Calculate pagination
|
|
$results['has_more'] = ($offset + count($rsvps)) < $total_count;
|
|
$results['next_offset'] = $offset + count($rsvps);
|
|
|
|
// If staging mode, simulate the sync
|
|
if ($this->is_staging) {
|
|
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
|
|
$results['synced'] = count($rsvps);
|
|
$results['test_data'] = array();
|
|
|
|
foreach ($rsvps as $rsvp) {
|
|
$rsvp_data = $this->prepare_rsvp_data($rsvp);
|
|
$results['test_data'][] = $rsvp_data;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
// Production sync
|
|
if (!$this->is_sync_allowed()) {
|
|
$results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.';
|
|
return $results;
|
|
}
|
|
|
|
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']++;
|
|
$results['errors'][] = sprintf('RSVP %s: No email address', $rsvp->ID);
|
|
continue;
|
|
}
|
|
|
|
// Step 1: Create/Update Lead
|
|
$lead_id = $this->ensure_lead_exists($rsvp_data);
|
|
if ($lead_id) {
|
|
$results['leads_created']++;
|
|
}
|
|
|
|
// Step 2: Create Campaign Member (link Lead to Campaign)
|
|
if ($lead_id && !empty($rsvp_data['event_id'])) {
|
|
$campaign_id = get_post_meta($rsvp_data['event_id'], '_zoho_campaign_id', true);
|
|
if ($campaign_id) {
|
|
$assoc_response = $this->create_campaign_member($lead_id, $campaign_id, 'Responded', 'Leads');
|
|
if (isset($assoc_response['status']) && $assoc_response['status'] === 'success') {
|
|
$results['campaign_members_created']++;
|
|
} elseif (isset($assoc_response['data'][0]['status']) && $assoc_response['data'][0]['status'] === 'success') {
|
|
$results['campaign_members_created']++;
|
|
} else {
|
|
$results['errors'][] = sprintf('RSVP %s: Failed to link to campaign. Response: %s', $rsvp->ID, json_encode($assoc_response));
|
|
if (isset($assoc_response['data'])) {
|
|
$results['responses'][] = array('type' => 'link_error', 'id' => $rsvp->ID, 'response' => $assoc_response);
|
|
}
|
|
}
|
|
} else {
|
|
$results['errors'][] = sprintf('RSVP %s: Event %s has no Zoho Campaign ID', $rsvp->ID, $rsvp_data['event_id']);
|
|
}
|
|
}
|
|
|
|
$results['synced']++;
|
|
|
|
// 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']++;
|
|
$results['errors'][] = sprintf('RSVP %s: %s', $rsvp->ID, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Ensure a Contact exists in Zoho (create or get existing, update fields)
|
|
*
|
|
* @param array $data Attendee data
|
|
* @return string|null Zoho Contact ID
|
|
*/
|
|
private function ensure_contact_exists($data) {
|
|
// Determine primary email for lookup
|
|
// Prefer measureQuick email, fallback to attendee email if mq_email is empty
|
|
// If both are empty, we can't create a contact
|
|
$attendee_email = !empty($data['email']) ? $data['email'] : '';
|
|
$mq_email = !empty($data['mq_email']) ? $data['mq_email'] : '';
|
|
|
|
// Use mq_email as primary if available, otherwise use attendee_email
|
|
$primary_email = !empty($mq_email) ? $mq_email : $attendee_email;
|
|
|
|
// Set secondary email (only if different from primary and not empty)
|
|
$other_email = '';
|
|
if (!empty($mq_email) && !empty($attendee_email) && $mq_email !== $attendee_email) {
|
|
$other_email = $attendee_email;
|
|
}
|
|
|
|
// Build contact data for create or update
|
|
$contact_data = array(
|
|
'First_Name' => $data['first_name'] ?: 'Unknown',
|
|
'Last_Name' => $data['last_name'] ?: 'Attendee',
|
|
'Email' => $primary_email,
|
|
'Lead_Source' => 'Event Tickets',
|
|
'Contact_Type' => 'Attendee',
|
|
'WordPress_Attendee_ID' => $data['attendee_id'],
|
|
);
|
|
|
|
// Add Other Email if we have both emails
|
|
if (!empty($other_email)) {
|
|
$contact_data['Secondary_Email'] = $other_email;
|
|
}
|
|
|
|
// Add Mobile phone if available
|
|
if (!empty($data['phone'])) {
|
|
$contact_data['Mobile'] = $data['phone'];
|
|
}
|
|
|
|
// Add Primary Role if available
|
|
if (!empty($data['company_role'])) {
|
|
$contact_data['Primary_Role'] = $data['company_role'];
|
|
}
|
|
|
|
// Search for existing contact by primary email
|
|
$search_response = $this->auth->make_api_request(
|
|
'/Contacts/search?criteria=(Email:equals:' . urlencode($primary_email) . ')',
|
|
'GET'
|
|
);
|
|
|
|
if (!empty($search_response['data'])) {
|
|
$contact_id = $search_response['data'][0]['id'];
|
|
|
|
// UPDATE existing contact with new field data
|
|
$this->auth->make_api_request("/Contacts/{$contact_id}", 'PUT', array(
|
|
'data' => array($contact_data)
|
|
));
|
|
|
|
return $contact_id;
|
|
}
|
|
|
|
// Create new contact
|
|
$create_response = $this->auth->make_api_request('/Contacts', 'POST', array(
|
|
'data' => array($contact_data)
|
|
));
|
|
|
|
if (!empty($create_response['data'][0]['details']['id'])) {
|
|
return $create_response['data'][0]['details']['id'];
|
|
}
|
|
|
|
// Handle DUPLICATE_DATA: Contact exists but with different email
|
|
// Extract existing contact ID from the error response and use it
|
|
if (!empty($create_response['data'][0]['code']) &&
|
|
$create_response['data'][0]['code'] === 'DUPLICATE_DATA' &&
|
|
!empty($create_response['data'][0]['details']['duplicate_record']['id'])) {
|
|
|
|
$existing_id = $create_response['data'][0]['details']['duplicate_record']['id'];
|
|
|
|
// Update the existing contact with new data
|
|
$this->auth->make_api_request("/Contacts/{$existing_id}", 'PUT', array(
|
|
'data' => array($contact_data)
|
|
));
|
|
|
|
return $existing_id;
|
|
}
|
|
|
|
// Store error for debugging and log it
|
|
$this->last_contact_error = "Email: {$primary_email}, Response: " . json_encode($create_response);
|
|
$this->auth->log_debug("Failed to create contact: " . $this->last_contact_error);
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Ensure a Lead exists in Zoho (create or get existing)
|
|
*
|
|
* @param array $data RSVP data
|
|
* @return string|null Zoho Lead ID
|
|
*/
|
|
private function ensure_lead_exists($data) {
|
|
// Search for existing lead by email
|
|
$search_response = $this->auth->make_api_request(
|
|
'/Leads/search?criteria=(Email:equals:' . urlencode($data['email']) . ')',
|
|
'GET'
|
|
);
|
|
|
|
if (!empty($search_response['data'])) {
|
|
return $search_response['data'][0]['id'];
|
|
}
|
|
|
|
// Create new lead
|
|
$lead_data = array(
|
|
'First_Name' => $data['first_name'] ?: 'Unknown',
|
|
'Last_Name' => $data['last_name'] ?: 'RSVP',
|
|
'Email' => $data['email'],
|
|
'Lead_Source' => 'Event RSVP',
|
|
'Lead_Status' => 'Contacted',
|
|
'WordPress_RSVP_ID' => $data['rsvp_id'],
|
|
);
|
|
|
|
$create_response = $this->auth->make_api_request('/Leads', 'POST', array(
|
|
'data' => array($lead_data)
|
|
));
|
|
|
|
if (!empty($create_response['data'][0]['details']['id'])) {
|
|
return $create_response['data'][0]['details']['id'];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create a Campaign Member (link Contact/Lead to Campaign)
|
|
*
|
|
* @param string $record_id Contact or Lead ID
|
|
* @param string $campaign_id Campaign ID
|
|
* @param string $status Member status (Invited, Responded, Attended, etc.)
|
|
* @param string $type 'Contacts' or 'Leads'
|
|
*/
|
|
private function create_campaign_member($record_id, $campaign_id, $status = 'Attended', $type = 'Contacts') {
|
|
// v6 API: Associate Contact/Lead with Campaign via related list
|
|
// PUT /{Contacts|Leads}/{record_id}/Campaigns
|
|
// The Contact/Lead is the parent, Campaigns is the related list
|
|
// Campaign ID goes in the request body, not the URL
|
|
|
|
$endpoint = "/{$type}/{$record_id}/Campaigns";
|
|
|
|
// Debug: Log the association attempt
|
|
$this->auth->log_debug("Attempting to associate {$type} {$record_id} with Campaign {$campaign_id} via {$endpoint}");
|
|
|
|
$response = $this->auth->make_api_request($endpoint, 'PUT', array(
|
|
'data' => array(
|
|
array(
|
|
'id' => $campaign_id,
|
|
'Member_Status' => $status
|
|
)
|
|
)
|
|
));
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Prepare campaign data for Zoho
|
|
*
|
|
* @param WP_Post $event Event post object
|
|
* @return array Campaign data
|
|
*/
|
|
private function prepare_campaign_data($event) {
|
|
$trainer_id = get_post_meta($event->ID, '_trainer_id', true);
|
|
$trainer = get_user_by('id', $trainer_id);
|
|
$venue_id = tribe_get_venue_id($event->ID);
|
|
|
|
// Get venue details
|
|
$venue_name = '';
|
|
$venue_city = '';
|
|
$venue_state = '';
|
|
$venue_details = '';
|
|
|
|
if ($venue_id) {
|
|
$venue_name = html_entity_decode(get_the_title($venue_id), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$venue_city = tribe_get_city($event->ID);
|
|
$venue_state = tribe_get_state($event->ID);
|
|
$venue_address = tribe_get_address($event->ID);
|
|
|
|
// Build venue details string
|
|
$venue_parts = array_filter(array($venue_name, $venue_address, $venue_city, $venue_state));
|
|
$venue_details = implode(', ', $venue_parts);
|
|
}
|
|
|
|
// Sanitize description: strip tags and limit length, then decode entities
|
|
$description = wp_strip_all_tags(get_the_content(null, false, $event));
|
|
$description = html_entity_decode($description, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
|
|
if (strlen($description) > 30000) {
|
|
$description = substr($description, 0, 30000) . '...';
|
|
}
|
|
|
|
$campaign_name = html_entity_decode(get_the_title($event->ID), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$trainer_name = $trainer ? html_entity_decode($trainer->display_name, ENT_QUOTES | ENT_HTML5, 'UTF-8') : '';
|
|
|
|
// Get trainer's Zoho Contact ID (if they've been synced)
|
|
$instructor_zoho_id = '';
|
|
if ($trainer) {
|
|
$instructor_zoho_id = get_user_meta($trainer->ID, '_zoho_contact_id', true);
|
|
}
|
|
|
|
// Calculate ticket counts
|
|
$total_capacity = intval(get_post_meta($event->ID, '_stock', true));
|
|
$tickets_sold = $this->get_attendee_count($event->ID);
|
|
$tickets_available = max(0, $total_capacity - $tickets_sold);
|
|
|
|
// Build campaign data array
|
|
$data = array(
|
|
'Campaign_Name' => substr($campaign_name, 0, 200),
|
|
'Start_Date' => tribe_get_start_date($event->ID, false, 'Y-m-d'),
|
|
'End_Date' => tribe_get_end_date($event->ID, false, 'Y-m-d'),
|
|
'Status' => (tribe_get_end_date($event->ID, false, 'U') < time()) ? 'Completed' : 'Active',
|
|
'Description' => $description,
|
|
'Type' => 'Training Event',
|
|
'Expected_Revenue' => floatval(get_post_meta($event->ID, '_price', true)),
|
|
'Total_Capacity' => $total_capacity,
|
|
'Tickets_Sold' => $tickets_sold,
|
|
'Tickets_Available' => $tickets_available,
|
|
'Event_City' => substr($venue_city, 0, 100),
|
|
'Event_State' => substr($venue_state, 0, 100),
|
|
'Event_URL' => get_permalink($event->ID),
|
|
'Venue_Details' => substr($venue_details, 0, 250),
|
|
'Instructor_Email' => $trainer ? $trainer->user_email : '',
|
|
);
|
|
|
|
// Add Instructor lookup only if we have a valid Zoho Contact ID
|
|
if (!empty($instructor_zoho_id)) {
|
|
$data['Instructor'] = $instructor_zoho_id;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Get count of attendees for an event
|
|
*
|
|
* @param int $event_id Event post ID
|
|
* @return int Attendee count
|
|
*/
|
|
private function get_attendee_count($event_id) {
|
|
// Check Tickets Commerce attendees
|
|
$tc_attendees = get_posts(array(
|
|
'post_type' => 'tec_tc_attendee',
|
|
'posts_per_page' => -1,
|
|
'post_status' => 'any',
|
|
'meta_query' => array(
|
|
array(
|
|
'key' => '_tec_tickets_commerce_event',
|
|
'value' => $event_id,
|
|
'compare' => '='
|
|
)
|
|
),
|
|
'fields' => 'ids'
|
|
));
|
|
|
|
// Also check PayPal attendees
|
|
$tpp_attendees = get_posts(array(
|
|
'post_type' => 'tribe_tpp_attendees',
|
|
'posts_per_page' => -1,
|
|
'post_status' => 'any',
|
|
'meta_query' => array(
|
|
array(
|
|
'key' => '_tribe_tpp_event',
|
|
'value' => $event_id,
|
|
'compare' => '='
|
|
)
|
|
),
|
|
'fields' => 'ids'
|
|
));
|
|
|
|
return count($tc_attendees) + count($tpp_attendees);
|
|
}
|
|
|
|
/**
|
|
* Prepare contact data for Zoho
|
|
*
|
|
* @param WP_User $user User object
|
|
* @return array Contact data
|
|
*/
|
|
private function prepare_contact_data($user) {
|
|
// Map WordPress roles to Zoho Contact_Type
|
|
if (in_array('hvac_master_trainer', $user->roles)) {
|
|
$role = 'Master Trainer';
|
|
} elseif (in_array('hvac_trainer', $user->roles)) {
|
|
$role = 'Trainer';
|
|
} else {
|
|
$role = 'Trainee';
|
|
}
|
|
|
|
return array(
|
|
'First_Name' => html_entity_decode(get_user_meta($user->ID, 'first_name', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
|
'Last_Name' => html_entity_decode(get_user_meta($user->ID, 'last_name', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
|
'Email' => $user->user_email,
|
|
'Phone' => get_user_meta($user->ID, 'phone_number', true),
|
|
'Title' => html_entity_decode(get_user_meta($user->ID, 'hvac_professional_title', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
|
'Company' => html_entity_decode(get_user_meta($user->ID, 'hvac_company_name', true), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
|
'Lead_Source' => 'HVAC Community Events',
|
|
'Contact_Type' => $role,
|
|
'WordPress_User_ID' => $user->ID,
|
|
'License_Number' => get_user_meta($user->ID, 'hvac_license_number', true),
|
|
'Years_Experience' => get_user_meta($user->ID, 'hvac_years_experience', true),
|
|
'Certification' => get_user_meta($user->ID, 'hvac_certifications', true)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepare invoice data for Tickets Commerce order
|
|
*
|
|
* @param WP_Post $order TC Order post object
|
|
* @return array Invoice data
|
|
*/
|
|
private function prepare_tc_invoice_data($order) {
|
|
$purchaser_email = get_post_meta($order->ID, '_tec_tc_order_purchaser_email', true);
|
|
$purchaser_name = get_post_meta($order->ID, '_tec_tc_order_purchaser_name', true);
|
|
$gateway = get_post_meta($order->ID, '_tec_tc_order_gateway', true);
|
|
$gateway_order_id = get_post_meta($order->ID, '_tec_tc_order_gateway_order_id', true);
|
|
$order_items = get_post_meta($order->ID, '_tec_tc_order_items', true);
|
|
|
|
// Parse order items to get event info and line items
|
|
$items = array();
|
|
$event_titles = array();
|
|
$total = 0;
|
|
|
|
if (is_array($order_items)) {
|
|
foreach ($order_items as $item) {
|
|
$event_id = isset($item['event_id']) ? $item['event_id'] : 0;
|
|
$ticket_id = isset($item['ticket_id']) ? $item['ticket_id'] : 0;
|
|
$quantity = isset($item['quantity']) ? intval($item['quantity']) : 1;
|
|
$price = isset($item['price']) ? floatval($item['price']) : 0;
|
|
|
|
if ($event_id) {
|
|
$event_titles[] = get_the_title($event_id);
|
|
}
|
|
|
|
$ticket_name = $ticket_id ? get_the_title($ticket_id) : 'Event Ticket';
|
|
|
|
$items[] = array(
|
|
'product' => array('name' => $ticket_name),
|
|
'quantity' => $quantity,
|
|
'list_price' => $price,
|
|
'total' => $price * $quantity
|
|
);
|
|
|
|
$total += $price * $quantity;
|
|
}
|
|
}
|
|
|
|
$event_summary = !empty($event_titles) ? implode(', ', array_unique($event_titles)) : 'Event Tickets';
|
|
|
|
return array(
|
|
'Subject' => "Ticket Purchase - {$event_summary}",
|
|
'Invoice_Date' => date('Y-m-d', strtotime($order->post_date)),
|
|
'Status' => 'Paid',
|
|
'Account_Name' => $purchaser_name,
|
|
'Description' => "Payment via {$gateway}. Transaction: {$gateway_order_id}",
|
|
'Grand_Total' => $total,
|
|
'WordPress_Order_ID' => $order->ID,
|
|
'Product_Details' => $items
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepare attendee data from Event Tickets attendee post
|
|
*
|
|
* @param WP_Post $attendee Attendee post object
|
|
* @return array Attendee data
|
|
*/
|
|
private function prepare_attendee_data($attendee) {
|
|
$post_type = $attendee->post_type;
|
|
|
|
// Handle different attendee post types
|
|
if ($post_type === 'tec_tc_attendee') {
|
|
// Tickets Commerce attendee (meta keys: _tec_tickets_commerce_*)
|
|
$event_id = get_post_meta($attendee->ID, '_tec_tickets_commerce_event', true);
|
|
$ticket_id = get_post_meta($attendee->ID, '_tec_tickets_commerce_ticket', true);
|
|
$order_id = get_post_meta($attendee->ID, '_tec_tickets_commerce_order', true);
|
|
$full_name = get_post_meta($attendee->ID, '_tec_tickets_commerce_full_name', true);
|
|
$email = get_post_meta($attendee->ID, '_tec_tickets_commerce_email', true);
|
|
$checkin = get_post_meta($attendee->ID, '_tec_tickets_commerce_checked_in', true);
|
|
|
|
// Custom attendee fields stored in _tec_tickets_commerce_attendee_fields (serialized array)
|
|
// Field names use hyphens: attendee-cell-phone, company-role, measurequick-email
|
|
$attendee_fields = get_post_meta($attendee->ID, '_tec_tickets_commerce_attendee_fields', true);
|
|
|
|
$phone = '';
|
|
$company_role = '';
|
|
$mq_email = '';
|
|
|
|
if (is_array($attendee_fields)) {
|
|
$phone = isset($attendee_fields['attendee-cell-phone']) ? $attendee_fields['attendee-cell-phone'] : '';
|
|
$company_role = isset($attendee_fields['company-role']) ? $attendee_fields['company-role'] : '';
|
|
$mq_email = isset($attendee_fields['measurequick-email']) ? $attendee_fields['measurequick-email'] : '';
|
|
}
|
|
|
|
} else {
|
|
// PayPal or other attendee types (tribe_tpp_attendees)
|
|
$event_id = get_post_meta($attendee->ID, '_tribe_tpp_event', true);
|
|
$ticket_id = get_post_meta($attendee->ID, '_tribe_tpp_product', true);
|
|
$order_id = get_post_meta($attendee->ID, '_tribe_tpp_order', true);
|
|
$full_name = get_post_meta($attendee->ID, '_tribe_tickets_full_name', true);
|
|
$email = get_post_meta($attendee->ID, '_tribe_tickets_email', true);
|
|
$checkin = get_post_meta($attendee->ID, '_tribe_tpp_checkin', true);
|
|
|
|
// Legacy attendee meta for custom fields (hyphenated keys)
|
|
$attendee_meta = get_post_meta($attendee->ID, '_tribe_tickets_meta', true);
|
|
$phone = is_array($attendee_meta) && isset($attendee_meta['attendee-cell-phone']) ? $attendee_meta['attendee-cell-phone'] : '';
|
|
$company_role = is_array($attendee_meta) && isset($attendee_meta['company-role']) ? $attendee_meta['company-role'] : '';
|
|
$mq_email = is_array($attendee_meta) && isset($attendee_meta['measurequick-email']) ? $attendee_meta['measurequick-email'] : '';
|
|
}
|
|
|
|
// Parse full name into first/last
|
|
$name_parts = explode(' ', trim($full_name), 2);
|
|
$first_name = isset($name_parts[0]) ? $name_parts[0] : '';
|
|
$last_name = isset($name_parts[1]) ? $name_parts[1] : '';
|
|
|
|
$event_title = $event_id ? get_the_title($event_id) : '';
|
|
$ticket_name = $ticket_id ? get_the_title($ticket_id) : '';
|
|
|
|
return array(
|
|
'attendee_id' => $attendee->ID,
|
|
'post_type' => $post_type,
|
|
'event_id' => $event_id,
|
|
'event_title' => $event_title,
|
|
'ticket_id' => $ticket_id,
|
|
'ticket_name' => $ticket_name,
|
|
'order_id' => $order_id,
|
|
'full_name' => $full_name,
|
|
'first_name' => $first_name,
|
|
'last_name' => $last_name,
|
|
'email' => $email,
|
|
'phone' => $phone,
|
|
'company_role' => $company_role,
|
|
'mq_email' => $mq_email,
|
|
'checked_in' => !empty($checkin),
|
|
'zoho_contact' => array(
|
|
'First_Name' => $first_name ?: 'Unknown',
|
|
'Last_Name' => $last_name ?: 'Attendee',
|
|
'Email' => $email,
|
|
'Lead_Source' => 'Event Tickets',
|
|
'Contact_Type' => 'Attendee',
|
|
),
|
|
'zoho_campaign_member' => array(
|
|
'Member_Status' => !empty($checkin) ? 'Attended' : 'Registered',
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepare RSVP data from Event Tickets RSVP attendee post
|
|
*
|
|
* @param WP_Post $rsvp RSVP attendee post object
|
|
* @return array RSVP data
|
|
*/
|
|
private function prepare_rsvp_data($rsvp) {
|
|
$event_id = get_post_meta($rsvp->ID, '_tribe_rsvp_event', true);
|
|
$ticket_id = get_post_meta($rsvp->ID, '_tribe_rsvp_product', true);
|
|
$full_name = get_post_meta($rsvp->ID, '_tribe_rsvp_full_name', true);
|
|
$email = get_post_meta($rsvp->ID, '_tribe_rsvp_email', true);
|
|
$rsvp_status = get_post_meta($rsvp->ID, '_tribe_rsvp_status', true); // yes, no
|
|
|
|
// Parse full name into first/last
|
|
$name_parts = explode(' ', trim($full_name), 2);
|
|
$first_name = isset($name_parts[0]) ? $name_parts[0] : '';
|
|
$last_name = isset($name_parts[1]) ? $name_parts[1] : '';
|
|
|
|
$event_title = $event_id ? get_the_title($event_id) : '';
|
|
|
|
return array(
|
|
'rsvp_id' => $rsvp->ID,
|
|
'event_id' => $event_id,
|
|
'event_title' => $event_title,
|
|
'ticket_id' => $ticket_id,
|
|
'full_name' => $full_name,
|
|
'first_name' => $first_name,
|
|
'last_name' => $last_name,
|
|
'email' => $email,
|
|
'rsvp_status' => $rsvp_status,
|
|
'zoho_lead' => array(
|
|
'First_Name' => $first_name ?: 'Unknown',
|
|
'Last_Name' => $last_name ?: 'RSVP',
|
|
'Email' => $email,
|
|
'Lead_Source' => 'Event RSVP',
|
|
'Lead_Status' => $rsvp_status === 'yes' ? 'Contacted' : 'Not Interested',
|
|
),
|
|
'zoho_campaign_member' => array(
|
|
'Member_Status' => $rsvp_status === 'yes' ? 'Responded' : 'Not Responded',
|
|
)
|
|
);
|
|
}
|
|
} |