feat: Add Zoho CRM integration with staging mode protection

- Implement OAuth 2.0 authentication for Zoho CRM
- Add sync functionality for Events → Campaigns, Users → Contacts, Orders → Invoices
- Create staging mode that prevents production syncs from non-production domains
- Build admin interface for sync management
- Add comprehensive field mapping between WordPress and Zoho
- Include test scripts and documentation
- Ensure production sync only on upskillhvac.com domain
This commit is contained in:
bengizmo 2025-05-19 13:17:44 -03:00
parent 5d45ed594d
commit 0e8b0f0325
41 changed files with 6941 additions and 50 deletions

View file

@ -1,12 +1,50 @@
#!/bin/bash
# Load environment variables from .env
source ../.env
if [ -f "$(dirname "$0")/../../.env" ]; then
source "$(dirname "$0")/../../.env"
elif [ -f ".env" ]; then
source ".env"
else
echo "Error: .env file not found"
exit 1
fi
# Define deployment variables
REMOTE_HOST="$UPSKILL_STAGING_IP"
REMOTE_USER="$UPSKILL_STAGING_SSH_USER"
REMOTE_PATH_BASE="$UPSKILL_STAGING_PATH"
PLUGIN_SLUG="hvac-community-events"
REMOTE_PLUGIN_PATH="$REMOTE_PATH_BASE/wp-content/plugins/$PLUGIN_SLUG"
LOCAL_PLUGIN_PATH="wordpress-dev/wordpress/wp-content/plugins/$PLUGIN_SLUG"
# Define files/directories to exclude
EXCLUDE_PATTERNS=(
".git/"
".gitignore"
"node_modules/"
".DS_Store"
"*.log"
".env"
".env.local"
"*.swp"
"*.tmp"
"tests/coverage/"
"tests/bin/"
"wordpress-dev/"
"*.md"
"composer.lock"
"package-lock.json"
)
# Define which directories to deploy
DEPLOY_DIRS=(
"includes"
"templates"
"assets"
)
# Define which root files to deploy
DEPLOY_FILES=(
"hvac-community-events.php"
"README.txt"
"index.php"
)

View file

@ -0,0 +1,91 @@
#!/bin/bash
# Script to disable Breeze cache for testing environments
# This creates an mu-plugin that sets DONOTCACHEPAGE constant
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Source environment variables
if [ -f "$PROJECT_ROOT/.env" ]; then
source "$PROJECT_ROOT/.env"
fi
# Check for required environment variables
if [ -z "$UPSKILL_STAGING_SSH_USER" ] || [ -z "$UPSKILL_STAGING_PASS" ] || [ -z "$UPSKILL_STAGING_IP" ] || [ -z "$UPSKILL_STAGING_PATH" ]; then
echo "Error: Missing required environment variables."
echo "Please ensure the following are set in your .env file:"
echo " - UPSKILL_STAGING_SSH_USER"
echo " - UPSKILL_STAGING_PASS"
echo " - UPSKILL_STAGING_IP"
echo " - UPSKILL_STAGING_PATH"
exit 1
fi
echo "Creating mu-plugin to disable Breeze cache for testing..."
# Create the mu-plugin content
MU_PLUGIN_CONTENT='<?php
/**
* Disable Breeze cache for testing environments
*/
// Disable Breeze cache when running tests
if (
getenv("WP_ENV") === "testing" ||
defined("WP_TESTS_DOMAIN") ||
strpos($_SERVER["HTTP_USER_AGENT"] ?? "", "Playwright") !== false ||
isset($_GET["no_cache_test"]) ||
(isset($_SERVER["REQUEST_URI"]) && strpos($_SERVER["REQUEST_URI"], "/manage-event/") !== false)
) {
if (!defined("DONOTCACHEPAGE")) {
define("DONOTCACHEPAGE", true);
}
}
// Additional Breeze filter to ensure cache is disabled
add_filter("breeze_override_donotcachepage", function($do_not_cache) {
if (
getenv("WP_ENV") === "testing" ||
defined("WP_TESTS_DOMAIN") ||
strpos($_SERVER["HTTP_USER_AGENT"] ?? "", "Playwright") !== false ||
isset($_GET["no_cache_test"])
) {
return false; // This returns false to the override, which means DO cache page = false
}
return $do_not_cache;
});
'
# Create mu-plugin via SSH
echo "Creating mu-plugin on staging server..."
sshpass -p "${UPSKILL_STAGING_PASS}" ssh -o StrictHostKeyChecking=no "${UPSKILL_STAGING_SSH_USER}@${UPSKILL_STAGING_IP}" \
"cd ${UPSKILL_STAGING_PATH} && \
mkdir -p wp-content/mu-plugins && \
cat > wp-content/mu-plugins/disable-breeze-for-tests.php << 'EOF'
$MU_PLUGIN_CONTENT
EOF"
# Verify the file was created
echo "Verifying mu-plugin creation..."
FILE_EXISTS=$(sshpass -p "${UPSKILL_STAGING_PASS}" ssh -o StrictHostKeyChecking=no "${UPSKILL_STAGING_SSH_USER}@${UPSKILL_STAGING_IP}" \
"cd ${UPSKILL_STAGING_PATH} && [ -f wp-content/mu-plugins/disable-breeze-for-tests.php ] && echo 'exists' || echo 'not found'")
if [ "$FILE_EXISTS" = "exists" ]; then
echo "✅ Successfully created mu-plugin to disable Breeze cache for testing"
echo "The following conditions will disable cache:"
echo " - WP_ENV environment variable is set to 'testing'"
echo " - WP_TESTS_DOMAIN constant is defined"
echo " - User agent contains 'Playwright'"
echo " - URL has 'no_cache_test' query parameter"
echo " - URL contains '/manage-event/'"
else
echo "❌ Failed to create mu-plugin"
exit 1
fi
# Clear existing cache
echo "Clearing existing Breeze cache..."
$SCRIPT_DIR/clear-breeze-cache.sh
echo "✅ Breeze cache setup for testing complete"

View file

@ -0,0 +1,61 @@
#!/bin/bash
# Zoho CRM Integration Test Script
# This script tests the Zoho integration setup
# Color codes for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Get the script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ZOHO_DIR="$PROJECT_ROOT/wordpress/wp-content/plugins/hvac-community-events/includes/zoho"
echo -e "${GREEN}=== Zoho CRM Integration Test ===${NC}\n"
# Check if .env file exists
if [ ! -f "$PROJECT_ROOT/.env" ]; then
echo -e "${RED}Error: .env file not found at $PROJECT_ROOT/.env${NC}"
exit 1
fi
# Source the .env file
source "$PROJECT_ROOT/.env"
# Check if credentials exist
if [ -z "$ZOHO_CLIENT_ID" ] || [ -z "$ZOHO_CLIENT_SECRET" ]; then
echo -e "${RED}Error: ZOHO_CLIENT_ID or ZOHO_CLIENT_SECRET not found in .env file${NC}"
exit 1
fi
echo -e "${GREEN}✓ Credentials found in .env file${NC}"
echo -e "Client ID: ${ZOHO_CLIENT_ID:0:20}...\n"
# Change to Zoho directory
cd "$ZOHO_DIR"
# Check if PHP is available
if ! command -v php &> /dev/null; then
echo -e "${RED}Error: PHP is not installed${NC}"
exit 1
fi
echo -e "${YELLOW}Starting Zoho integration test...${NC}\n"
# Option to start auth server
echo -e "${YELLOW}Would you like to start the OAuth callback server? (y/n)${NC}"
read -p "This will help capture the authorization code automatically: " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo -e "\n${GREEN}Starting OAuth callback server...${NC}"
echo -e "${YELLOW}Keep this terminal open and start a new terminal for the next step.${NC}\n"
php "$ZOHO_DIR/auth-server.php"
else
echo -e "\n${GREEN}Running test integration script...${NC}"
php "$ZOHO_DIR/test-integration.php"
fi
echo -e "\n${GREEN}=== Test Complete ===${NC}"

View file

@ -0,0 +1,34 @@
#!/bin/bash
# Zoho OAuth Setup Helper Script
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== Zoho OAuth Setup Helper ===${NC}"
echo
echo -e "${YELLOW}This script will guide you through the Zoho OAuth setup process.${NC}"
echo
echo -e "${GREEN}Step 1: Open Authorization URL${NC}"
echo "Open the following URL in your browser:"
echo
echo "https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.settings.all%2CZohoCRM.modules.all%2CZohoCRM.users.all%2CZohoCRM.org.all&client_id=1000.Z0HOF1VMMJ9W2QWSU57GVQYEAVUSKS&response_type=code&access_type=offline&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&prompt=consent"
echo
echo -e "${GREEN}Step 2: Authorize and Get Code${NC}"
echo "1. Log in to Zoho if prompted"
echo "2. Review and accept the permissions"
echo "3. You'll be redirected to: http://localhost:8080/callback?code=AUTH_CODE"
echo "4. Copy the 'code' parameter from the URL"
echo
echo -e "${GREEN}Step 3: Run Integration Test${NC}"
echo "Once you have the code, run:"
echo "cd /Users/ben/dev/upskill-event-manager/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho"
echo "php test-integration.php"
echo
echo "Then paste the authorization code when prompted."
echo
echo -e "${BLUE}Note: The authorization code expires quickly, so complete the process promptly.${NC}"

View file

@ -0,0 +1,60 @@
#!/bin/bash
# Complete Zoho OAuth Setup Script
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== Complete Zoho OAuth Setup ===${NC}"
echo
PLUGIN_DIR="/Users/ben/dev/upskill-event-manager/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events"
ZOHO_DIR="$PLUGIN_DIR/includes/zoho"
# Step 1: Start callback server
echo -e "${GREEN}Starting callback server...${NC}"
cd "$ZOHO_DIR"
php -S localhost:8080 callback-server.php &
SERVER_PID=$!
echo "Callback server started (PID: $SERVER_PID)"
echo
# Step 2: Display authorization URL
echo -e "${GREEN}Please complete authorization:${NC}"
echo
echo -e "${YELLOW}1. Open this URL in your browser:${NC}"
echo
echo "https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.settings.all%2CZohoCRM.modules.all%2CZohoCRM.users.all%2CZohoCRM.org.all&client_id=1000.Z0HOF1VMMJ9W2QWSU57GVQYEAVUSKS&response_type=code&access_type=offline&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&prompt=consent"
echo
echo -e "${YELLOW}2. Log in to Zoho and accept permissions${NC}"
echo -e "${YELLOW}3. Copy the authorization code from the callback page${NC}"
echo -e "${YELLOW}4. Return here and paste the code${NC}"
echo
# Step 3: Wait for callback
echo -e "${GREEN}Waiting for authorization...${NC}"
echo "Once you've authorized, you'll see the code at: http://localhost:8080/callback"
echo
read -p "Enter the authorization code: " AUTH_CODE
# Step 4: Stop callback server
kill $SERVER_PID 2>/dev/null || true
echo
# Step 5: Run integration test with the code
echo -e "${GREEN}Testing integration with authorization code...${NC}"
cd "$ZOHO_DIR"
echo "$AUTH_CODE" | php test-integration.php
echo
echo -e "${GREEN}✓ Setup complete!${NC}"
echo
echo "The Zoho configuration has been saved to:"
echo "$ZOHO_DIR/zoho-config.php"
echo
echo "You can now sync data to Zoho CRM!"

View file

@ -0,0 +1,10 @@
# Ignore Zoho credentials
includes/zoho/zoho-config.php
# Ignore log files
*.log
# Development files
.DS_Store
node_modules/
vendor/

View file

@ -0,0 +1,56 @@
/**
* Zoho CRM Admin Styles
*/
.hvac-zoho-status,
.hvac-zoho-sync,
.hvac-zoho-settings {
margin-top: 30px;
background: #fff;
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
}
.sync-section {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.sync-section:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.sync-section h3 {
margin-top: 0;
}
.sync-status {
margin-top: 10px;
}
.sync-status .notice {
margin: 10px 0;
}
#connection-status {
margin-top: 10px;
}
#connection-status .notice {
margin: 10px 0;
}
.sync-button {
margin-top: 10px;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', Courier, monospace;
}

View file

@ -0,0 +1,128 @@
/**
* Zoho CRM Admin JavaScript
*/
jQuery(document).ready(function($) {
// Test connection
$('#test-connection').on('click', function() {
var $button = $(this);
var $status = $('#connection-status');
$button.prop('disabled', true).text('Testing...');
$status.html('');
$.ajax({
url: hvacZoho.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_zoho_test_connection',
nonce: hvacZoho.nonce
},
success: function(response) {
if (response.success) {
$status.html('<div class="notice notice-success"><p>' + response.data.message + ' (' + response.data.modules + ')</p></div>');
} else {
$status.html('<div class="notice notice-error"><p>' + response.data.message + ': ' + response.data.error + '</p></div>');
}
},
error: function() {
$status.html('<div class="notice notice-error"><p>Connection test failed</p></div>');
},
complete: function() {
$button.prop('disabled', false).text('Test Connection');
}
});
});
// Sync data
$('.sync-button').on('click', function() {
var $button = $(this);
var type = $button.data('type');
var $status = $('#' + type + '-status');
$button.prop('disabled', true).text('Syncing...');
$status.html('<p>Syncing ' + type + '...</p>');
$.ajax({
url: hvacZoho.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_zoho_sync_data',
type: type,
nonce: hvacZoho.nonce
},
success: function(response) {
if (response.success) {
var result = response.data;
var html = '<div class="notice notice-success">';
if (result.staging_mode) {
html += '<h4>🔧 STAGING MODE - Simulation Results</h4>';
html += '<p>' + result.message + '</p>';
} else {
html += '<p>Sync completed successfully!</p>';
}
html += '<ul>' +
'<li>Total records: ' + result.total + '</li>' +
'<li>Synced: ' + result.synced + '</li>' +
'<li>Failed: ' + result.failed + '</li>' +
'</ul>';
if (result.test_data && result.test_data.length > 0) {
html += '<details>' +
'<summary>View test data (first 5 records)</summary>' +
'<pre style="background: #f0f0f0; padding: 10px; overflow: auto;">' +
JSON.stringify(result.test_data.slice(0, 5), null, 2) +
'</pre>' +
'</details>';
}
html += '</div>';
$status.html(html);
} else {
$status.html('<div class="notice notice-error"><p>' + response.data.message + ': ' + response.data.error + '</p></div>');
}
},
error: function() {
$status.html('<div class="notice notice-error"><p>Sync failed</p></div>');
},
complete: function() {
$button.prop('disabled', false).text('Sync ' + type.charAt(0).toUpperCase() + type.slice(1));
}
});
});
// Save settings
$('#zoho-settings-form').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $button = $form.find('button[type="submit"]');
$button.prop('disabled', true).text('Saving...');
$.ajax({
url: hvacZoho.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_zoho_save_settings',
nonce: hvacZoho.nonce,
auto_sync: $form.find('input[name="auto_sync"]').is(':checked') ? '1' : '0',
sync_frequency: $form.find('select[name="sync_frequency"]').val()
},
success: function(response) {
if (response.success) {
alert('Settings saved successfully!');
} else {
alert('Error saving settings: ' + response.data.message);
}
},
error: function() {
alert('Error saving settings');
},
complete: function() {
$button.prop('disabled', false).text('Save Settings');
}
});
});
});

View file

@ -0,0 +1,54 @@
#!/bin/bash
# HVAC Community Events Test Runner
# This script runs the plugin's test suite
# Set up colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
echo -e "${GREEN}HVAC Community Events Test Suite${NC}"
echo "=================================="
# Check if PHPUnit is available
if ! command -v phpunit &> /dev/null; then
echo -e "${RED}Error: PHPUnit is not installed or not in PATH${NC}"
echo "Please install PHPUnit or use the vendor/bin/phpunit"
exit 1
fi
# Change to plugin directory
cd "$PLUGIN_DIR"
# Run different test suites based on arguments
if [ "$1" = "unit" ]; then
echo -e "${YELLOW}Running Unit Tests...${NC}"
phpunit --testsuite=unit
elif [ "$1" = "integration" ]; then
echo -e "${YELLOW}Running Integration Tests...${NC}"
phpunit --testsuite=integration
elif [ "$1" = "coverage" ]; then
echo -e "${YELLOW}Running Tests with Code Coverage...${NC}"
phpunit --coverage-html coverage-report --coverage-text
echo -e "${GREEN}Coverage report generated in coverage-report/${NC}"
elif [ "$1" = "specific" ] && [ -n "$2" ]; then
echo -e "${YELLOW}Running Specific Test: $2${NC}"
phpunit "$2"
else
echo -e "${YELLOW}Running All Tests...${NC}"
phpunit
fi
# Check exit status
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ Tests passed successfully!${NC}"
else
echo -e "${RED}✗ Tests failed!${NC}"
exit 1
fi

View file

@ -44,7 +44,7 @@ function hvac_ce_create_required_pages() {
],
'hvac-dashboard' => [
'title' => 'Trainer Dashboard',
'content' => '', // Content handled by template or redirect
'content' => '<!-- wp:shortcode -->[hvac_trainer_dashboard]<!-- /wp:shortcode -->',
],
'manage-event' => [ // New page for TEC CE submission form shortcode
'title' => 'Manage Event',
@ -217,4 +217,5 @@ function hvac_ce_include_order_summary_template( $template ) {
}
return $template;
}
add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 );
// Removed - template handling is now in the main class
// add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 );

View file

@ -0,0 +1,231 @@
<?php
/**
* Zoho CRM Admin Interface
*
* @package HVACCommunityEvents
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Zoho CRM Admin Class
*/
class HVAC_Zoho_Admin {
/**
* Initialize the admin interface
*/
public function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
add_action('wp_ajax_hvac_zoho_test_connection', array($this, 'test_connection'));
add_action('wp_ajax_hvac_zoho_sync_data', array($this, 'sync_data'));
}
/**
* Add admin menu
*/
public function add_admin_menu() {
add_submenu_page(
'hvac-community-events',
'Zoho CRM Sync',
'Zoho CRM Sync',
'manage_options',
'hvac-zoho-sync',
array($this, 'render_admin_page')
);
}
/**
* Enqueue admin scripts
*/
public function enqueue_admin_scripts($hook) {
if ($hook !== 'hvac-community-events_page_hvac-zoho-sync') {
return;
}
wp_enqueue_script(
'hvac-zoho-admin',
HVAC_PLUGIN_URL . 'assets/js/zoho-admin.js',
array('jquery'),
HVAC_VERSION,
true
);
wp_localize_script('hvac-zoho-admin', 'hvacZoho', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_zoho_nonce')
));
wp_enqueue_style(
'hvac-zoho-admin',
HVAC_PLUGIN_URL . 'assets/css/zoho-admin.css',
array(),
HVAC_VERSION
);
}
/**
* Render admin page
*/
public function render_admin_page() {
$config_file = HVAC_PLUGIN_DIR . 'includes/zoho/zoho-config.php';
$is_configured = file_exists($config_file);
$site_url = get_site_url();
$is_staging = strpos($site_url, 'upskillhvac.com') === false;
?>
<div class="wrap">
<h1>Zoho CRM Sync</h1>
<?php if ($is_staging): ?>
<div class="notice notice-info">
<h3>🔧 STAGING MODE ACTIVE</h3>
<p><strong>Current site:</strong> <?php echo esc_html($site_url); ?></p>
<p>Staging mode is active. Data sync will be simulated only. No actual data will be sent to Zoho CRM.</p>
<p>Production sync is only enabled on <strong>upskillhvac.com</strong></p>
</div>
<?php endif; ?>
<?php if (!$is_configured): ?>
<div class="notice notice-warning">
<p>Zoho CRM is not configured. Please complete the OAuth setup first.</p>
<p>Run: <code>./bin/zoho-setup-complete.sh</code></p>
</div>
<?php else: ?>
<div class="hvac-zoho-status">
<h2>Connection Status</h2>
<button class="button button-primary" id="test-connection">Test Connection</button>
<div id="connection-status"></div>
</div>
<div class="hvac-zoho-sync">
<h2>Data Sync</h2>
<div class="sync-section">
<h3>Events Campaigns</h3>
<p>Sync events from The Events Calendar to Zoho CRM Campaigns</p>
<button class="button sync-button" data-type="events">Sync Events</button>
<div class="sync-status" id="events-status"></div>
</div>
<div class="sync-section">
<h3>Users Contacts</h3>
<p>Sync trainers and attendees to Zoho CRM Contacts</p>
<button class="button sync-button" data-type="users">Sync Users</button>
<div class="sync-status" id="users-status"></div>
</div>
<div class="sync-section">
<h3>Purchases Invoices</h3>
<p>Sync ticket purchases to Zoho CRM Invoices</p>
<button class="button sync-button" data-type="purchases">Sync Purchases</button>
<div class="sync-status" id="purchases-status"></div>
</div>
</div>
<div class="hvac-zoho-settings">
<h2>Sync Settings</h2>
<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>
</form>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Test Zoho connection
*/
public function test_connection() {
check_ajax_referer('hvac_zoho_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
try {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-crm-auth.php';
$auth = new HVAC_Zoho_CRM_Auth();
// Test API call
$response = $auth->make_api_request('GET', '/crm/v2/settings/modules');
if ($response && !isset($response['error'])) {
wp_send_json_success(array(
'message' => 'Connection successful!',
'modules' => count($response['modules']) . ' modules available'
));
} else {
wp_send_json_error(array(
'message' => 'Connection failed',
'error' => isset($response['error']) ? $response['error'] : 'Unknown error'
));
}
} catch (Exception $e) {
wp_send_json_error(array(
'message' => 'Connection failed',
'error' => $e->getMessage()
));
}
}
/**
* Sync data to Zoho
*/
public function sync_data() {
check_ajax_referer('hvac_zoho_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$type = sanitize_text_field($_POST['type']);
try {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-sync.php';
$sync = new HVAC_Zoho_Sync();
switch ($type) {
case 'events':
$result = $sync->sync_events();
break;
case 'users':
$result = $sync->sync_users();
break;
case 'purchases':
$result = $sync->sync_purchases();
break;
default:
throw new Exception('Invalid sync type');
}
wp_send_json_success($result);
} catch (Exception $e) {
wp_send_json_error(array(
'message' => 'Sync failed',
'error' => $e->getMessage()
));
}
}
}
// Initialize the admin interface
new HVAC_Zoho_Admin();
?>

View file

@ -0,0 +1,96 @@
<?php
/**
* Event Author Fixer
*
* Ensures events created through Community Events are properly assigned to the creating user
*
* @package HVAC_Community_Events
*/
namespace HVAC_Community_Events;
/**
* Class Event_Author_Fixer
*
* Fixes event author assignment for Community Events
*/
class Event_Author_Fixer {
/**
* Constructor
*/
public function __construct() {
// Hook into Community Events submission process
add_filter('tec_events_community_before_save_submission', array($this, 'ensure_author_is_set'), 10, 1);
// Hook into event creation to ensure author is current user
add_action('tribe_community_event_created', array($this, 'fix_event_author'), 10, 1);
// Also hook into WordPress post data to ensure author is set
add_filter('wp_insert_post_data', array($this, 'set_post_author'), 10, 2);
}
/**
* Ensure author is set in submission data before saving
*
* @param array $submission The submission data
* @return array Modified submission data
*/
public function ensure_author_is_set($submission) {
// If post_author is not set or is 0, set it to current user
if (empty($submission['post_author']) || $submission['post_author'] == 0) {
$current_user_id = get_current_user_id();
if ($current_user_id > 0) {
$submission['post_author'] = $current_user_id;
HVAC_Logger::info('Setting event author to current user: ' . $current_user_id, 'EventAuthor');
}
}
return $submission;
}
/**
* Fix event author after creation
*
* @param int $event_id The event ID
*/
public function fix_event_author($event_id) {
$event = get_post($event_id);
if ($event && $event->post_type === 'tribe_events') {
$current_user_id = get_current_user_id();
// If the event has no author or author is 0, set it to current user
if (($event->post_author == 0 || empty($event->post_author)) && $current_user_id > 0) {
wp_update_post(array(
'ID' => $event_id,
'post_author' => $current_user_id
));
HVAC_Logger::info('Fixed event author for event ' . $event_id . ' to user ' . $current_user_id, 'EventAuthor');
}
}
}
/**
* Set post author when inserting tribe_events post
*
* @param array $data The post data
* @param array $postarr The post array
* @return array Modified post data
*/
public function set_post_author($data, $postarr) {
// Only handle tribe_events posts
if ($data['post_type'] === 'tribe_events') {
$current_user_id = get_current_user_id();
// If no author is set and we have a logged-in user, set the author
if ((empty($data['post_author']) || $data['post_author'] == 0) && $current_user_id > 0) {
$data['post_author'] = $current_user_id;
HVAC_Logger::info('Setting tribe_events post author to: ' . $current_user_id, 'EventAuthor');
}
}
return $data;
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* Event Form Handler
*
* @package HVAC_Community_Events
*/
namespace HVAC_Community_Events;
/**
* Class Event_Form_Handler
*
* Handles event form submission field mapping
*/
class Event_Form_Handler {
/**
* Constructor
*/
public function __construct() {
add_filter('tec_events_community_submission_form_data', array($this, 'map_description_field'), 10, 1);
add_filter('tec_events_community_submission_validate_before', array($this, 'map_description_before_validation'), 5, 1);
}
/**
* Map tcepostcontent to post_content before validation
*
* @param array $submission_data The form submission data
* @return array Modified submission data
*/
public function map_description_before_validation($submission_data) {
// If tcepostcontent exists but post_content doesn't, map it
if (isset($submission_data['tcepostcontent']) && empty($submission_data['post_content'])) {
$submission_data['post_content'] = $submission_data['tcepostcontent'];
}
return $submission_data;
}
/**
* Map description field for form data
*
* @param array $form_data The form data
* @return array Modified form data
*/
public function map_description_field($form_data) {
// Ensure post_content is set from tcepostcontent
if (isset($_POST['tcepostcontent']) && empty($_POST['post_content'])) {
$_POST['post_content'] = $_POST['tcepostcontent'];
$form_data['post_content'] = $_POST['tcepostcontent'];
}
return $form_data;
}
}

View file

@ -54,7 +54,10 @@ class HVAC_Community_Events {
'class-hvac-settings.php',
'community/class-login-handler.php',
'community/class-event-handler.php',
'class-hvac-dashboard-data.php'
'class-hvac-dashboard-data.php',
'class-event-form-handler.php', // Add our form handler
'class-event-author-fixer.php', // Fix event author assignment
'class-hvac-dashboard.php' // New dashboard handler
];
foreach ($files_to_include as $file) {
$path = HVAC_CE_PLUGIN_DIR . 'includes/' . $file;
@ -65,6 +68,16 @@ class HVAC_Community_Events {
HVAC_Logger::error("Failed to include file: {$file} - File not found", 'Core');
}
}
// Load Zoho integration if in admin
if (is_admin()) {
$zoho_path = HVAC_CE_PLUGIN_DIR . 'includes/admin/class-zoho-admin.php';
if (file_exists($zoho_path)) {
require_once $zoho_path;
HVAC_Logger::info("Included Zoho admin interface", 'Core');
}
}
HVAC_Logger::info('All required files loaded', 'Core');
}
@ -80,8 +93,8 @@ class HVAC_Community_Events {
// Initialize other hooks
add_action('init', array($this, 'init'));
// Template loading for custom pages
add_filter('template_include', array($this, 'load_custom_templates'));
// Template loading for custom pages (removed - using content filter instead)
// add_filter('template_include', array($this, 'load_custom_templates'));
} // End init_hooks
/**
@ -98,62 +111,121 @@ class HVAC_Community_Events {
public static function deactivate() {
// Remove the hvac_trainer role
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php'; // Ensure class is available
$roles = new HVAC_Roles();
$roles->remove_trainer_role();
// Additional deactivation tasks
// ...
HVAC_Roles::remove_hvac_trainer_role();
HVAC_Logger::info('Deactivation completed: HVAC trainer role removed.', 'Core');
}
/**
* Initialize plugin actions attached to 'init' hook
* Initialize function (hooked on 'init')
*/
public function init() {
HVAC_Logger::info('Init method started', 'Core');
// Initialize handlers
new \HVAC_Community_Events\Community\Login_Handler();
HVAC_Logger::info('Login_Handler initialized', 'Core');
new HVAC_Registration();
HVAC_Logger::info('HVAC_Registration initialized', 'Core');
HVAC_Logger::info('Init method completed', 'Core');
// Prevent trainers from accessing wp-admin
add_action('admin_init', array($this, 'redirect_trainers_from_admin'));
}
/**
* Redirect HVAC trainers from admin area to frontend dashboard
*/
public function redirect_trainers_from_admin() {
if (defined('DOING_AJAX') && DOING_AJAX) {
return;
}
// Check if user is trying to access wp-admin and has trainer role but not admin caps
if ( is_admin() && ! current_user_can('manage_options') && current_user_can('view_hvac_dashboard') ) {
wp_redirect(home_url('/hvac-dashboard/')); // Corrected slug
exit;
// Initialize roles
$this->init_roles();
// Initialize forms
$this->init_forms();
// Initialize shortcodes
$this->init_shortcodes();
// Initialize event form handler
if (class_exists('HVAC_Community_Events\Event_Form_Handler')) {
new \HVAC_Community_Events\Event_Form_Handler();
}
}
/**
* Load custom templates for plugin pages.
*
* @param string $template The path of the template to include.
* @return string The path of the template to include.
* Initialize roles
*/
public function load_custom_templates( $template ) {
// Check if we are on the HVAC Dashboard page
if ( is_page( 'hvac-dashboard' ) ) {
$new_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php';
if ( file_exists( $new_template ) ) {
return $new_template;
private function init_roles() {
$roles = new HVAC_Roles();
// Note: Role creation is handled in the activate method or the class constructor
}
/**
* Initialize forms
*/
private function init_forms() {
$registration = new HVAC_Registration();
// Note: Form registration is handled in the class constructor
}
/**
* Initialize shortcodes
*/
private function init_shortcodes() {
// Registration form shortcode
add_shortcode('hvac_trainer_registration', array('HVAC_Registration', 'render_registration_form'));
// Community login shortcode
add_shortcode('hvac_community_login', array('HVAC_Community_Login_Handler', 'render_login_form'));
// Dashboard shortcode
add_shortcode('hvac_dashboard', array($this, 'render_dashboard'));
// Add the event summary shortcode
add_shortcode('hvac_event_summary', array($this, 'render_event_summary'));
// Remove the event form shortcode as we're using TEC's shortcode instead
// add_shortcode('hvac_event_form', array('HVAC_Community_Event_Handler', 'render_event_form'));
// Add future shortcodes here
}
/**
* Render dashboard content
*/
public function render_dashboard() {
if (!is_user_logged_in()) {
return '<p>Please log in to view the dashboard.</p>';
}
// Include the dashboard template
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/dashboard/trainer-dashboard.php';
return ob_get_clean();
}
/**
* Render event summary content
*/
public function render_event_summary() {
// This can be used to display custom event summary content
return '<div class="hvac-event-summary">Event Summary Content Here</div>';
}
/**
* Include custom templates for plugin pages
*/
public function load_custom_templates($template) {
// Check for dashboard page
if (is_page('hvac-dashboard')) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php';
if (file_exists($custom_template)) {
return $custom_template;
}
}
// Add checks for other custom pages here if needed
// Check for my-events page
if (is_page('my-events')) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/page-my-events.php';
if (file_exists($custom_template)) {
return $custom_template;
}
}
// Check for single event view (temporary)
if ( is_singular( 'tribe_events' ) ) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/single-tribe_events.php';
if ( file_exists( $custom_template ) ) {
return $custom_template;
}
}
// Add future custom templates here
return $template;
}
} // End load_custom_templates
} // End class HVAC_Community_Events

View file

@ -0,0 +1,243 @@
<?php
/**
* HVAC Community Events Dashboard Data Handler - Fixed Version
*
* Consistently queries by post_author for trainer's events
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 1.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Dashboard_Data_Fixed
*/
class HVAC_Dashboard_Data_Fixed {
/**
* The ID of the trainer user.
*
* @var int
*/
private int $user_id;
/**
* Constructor.
*
* @param int $user_id The ID of the trainer user.
*/
public function __construct( int $user_id ) {
$this->user_id = $user_id;
}
/**
* Get the total number of events created by the trainer.
*
* @return int
*/
public function get_total_events_count() : int {
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $this->user_id,
'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
);
$query = new WP_Query( $args );
return (int) $query->found_posts;
}
/**
* Get the number of upcoming events for the trainer.
*
* @return int
*/
public function get_upcoming_events_count() : int {
$today = current_time( 'mysql' );
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $this->user_id, // Use author consistently
'post_status' => array( 'publish', 'future' ),
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => '_EventStartDate',
'value' => $today,
'compare' => '>=',
'type' => 'DATETIME',
),
),
'orderby' => 'meta_value',
'meta_key' => '_EventStartDate',
'order' => 'ASC',
);
$query = new WP_Query( $args );
return (int) $query->found_posts;
}
/**
* Get the number of past events for the trainer.
*
* @return int
*/
public function get_past_events_count() : int {
$today = current_time( 'mysql' );
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $this->user_id, // Use author consistently
'post_status' => array( 'publish', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => '_EventEndDate',
'value' => $today,
'compare' => '<',
'type' => 'DATETIME',
),
),
);
$query = new WP_Query( $args );
return (int) $query->found_posts;
}
/**
* Get the total number of tickets sold across all the trainer's events.
*
* @return int
*/
public function get_total_tickets_sold() : int {
$total_tickets = 0;
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $this->user_id, // Use author consistently
'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
);
$event_ids = get_posts( $args );
if ( ! empty( $event_ids ) ) {
foreach ( $event_ids as $event_id ) {
$sold = get_post_meta( $event_id, '_tribe_tickets_sold', true );
if ( is_numeric( $sold ) ) {
$total_tickets += (int) $sold;
}
}
}
return $total_tickets;
}
/**
* Get the total revenue generated across all the trainer's events.
*
* @return float
*/
public function get_total_revenue() : float {
$total_revenue = 0.0;
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $this->user_id, // Use author consistently
'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
);
$event_ids = get_posts( $args );
if ( ! empty( $event_ids ) ) {
foreach ( $event_ids as $event_id ) {
$revenue = get_post_meta( $event_id, '_tribe_revenue_total', true );
if ( is_numeric( $revenue ) ) {
$total_revenue += (float) $revenue;
}
}
}
return $total_revenue;
}
/**
* Get the annual revenue target set by the trainer.
*
* @return float|null Returns the target as a float, or null if not set.
*/
public function get_annual_revenue_target() : ?float {
$target = get_user_meta( $this->user_id, 'annual_revenue_target', true );
return ! empty( $target ) && is_numeric( $target ) ? (float) $target : null;
}
/**
* Get the data needed for the events table on the dashboard.
*
* @param string $filter_status The status to filter events by.
* @return array An array of event data arrays.
*/
public function get_events_table_data( string $filter_status = 'all' ) : array {
$events_data = [];
$valid_statuses = array( 'publish', 'future', 'draft', 'pending', 'private' );
$post_status = ( 'all' === $filter_status || ! in_array( $filter_status, $valid_statuses, true ) )
? $valid_statuses
: array( $filter_status );
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $this->user_id, // Use author consistently
'post_status' => $post_status,
'posts_per_page' => -1,
'orderby' => 'meta_value',
'meta_key' => '_EventStartDate',
'order' => 'DESC',
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$event_id = get_the_ID();
// Get Capacity
$total_capacity = 0;
if ( function_exists( 'tribe_get_tickets' ) ) {
$tickets = tribe_get_tickets( $event_id );
if ( $tickets ) {
foreach ( $tickets as $ticket ) {
$capacity = $ticket->capacity();
if ( $capacity === -1 ) {
$total_capacity = -1;
break;
}
if ( is_numeric( $capacity ) ) {
$total_capacity += $capacity;
}
}
}
}
$sold = get_post_meta( $event_id, '_tribe_tickets_sold', true );
$revenue = get_post_meta( $event_id, '_tribe_revenue_total', true );
$events_data[] = array(
'id' => $event_id,
'status' => get_post_status( $event_id ),
'name' => get_the_title(),
'link' => get_permalink( $event_id ),
'start_date_ts' => strtotime( get_post_meta( $event_id, '_EventStartDate', true ) ),
'organizer_id' => (int) get_post_meta( $event_id, '_EventOrganizerID', true ),
'capacity' => ( $total_capacity === -1 ) ? 'Unlimited' : (int) $total_capacity,
'sold' => is_numeric( $sold ) ? (int) $sold : 0,
'revenue' => is_numeric( $revenue ) ? (float) $revenue : 0.0,
);
}
wp_reset_postdata();
}
return $events_data;
}
}

View file

@ -0,0 +1,335 @@
<?php
/**
* HVAC Community Events Dashboard Data Handler - Refactored
*
* Optimized version with better caching and query optimization
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 1.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Dashboard_Data_Refactored
*/
class HVAC_Dashboard_Data_Refactored {
/**
* The ID of the trainer user.
*
* @var int
*/
private int $user_id;
/**
* Cache group for dashboard data
*
* @var string
*/
private $cache_group = 'hvac_dashboard';
/**
* Cache expiration time (5 minutes)
*
* @var int
*/
private $cache_expiration = 300;
/**
* Constructor.
*
* @param int $user_id The ID of the trainer user.
*/
public function __construct( int $user_id ) {
$this->user_id = $user_id;
}
/**
* Get all dashboard stats in a single cached object
*
* @return array
*/
public function get_all_stats() : array {
$cache_key = 'stats_' . $this->user_id;
$stats = wp_cache_get( $cache_key, $this->cache_group );
if ( false === $stats ) {
$stats = array(
'total_events' => $this->calculate_total_events_count(),
'upcoming_events' => $this->calculate_upcoming_events_count(),
'past_events' => $this->calculate_past_events_count(),
'total_tickets' => $this->calculate_total_tickets_sold(),
'total_revenue' => $this->calculate_total_revenue(),
'revenue_target' => $this->get_annual_revenue_target(),
);
wp_cache_set( $cache_key, $stats, $this->cache_group, $this->cache_expiration );
HVAC_Logger::info( 'Dashboard stats calculated and cached', 'Dashboard', array( 'user_id' => $this->user_id ) );
}
return $stats;
}
/**
* Clear cache for a specific user
*
* @return void
*/
public function clear_cache() {
$cache_key = 'stats_' . $this->user_id;
wp_cache_delete( $cache_key, $this->cache_group );
HVAC_Logger::info( 'Dashboard cache cleared', 'Dashboard', array( 'user_id' => $this->user_id ) );
}
/**
* Calculate total events count (optimized)
*
* @return int
*/
private function calculate_total_events_count() : int {
global $wpdb;
// Direct query is more efficient for simple counts
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(ID) FROM {$wpdb->posts}
WHERE post_type = %s
AND post_author = %d
AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')",
Tribe__Events__Main::POSTTYPE,
$this->user_id
) );
return (int) $count;
}
/**
* Calculate upcoming events count
*
* @return int
*/
private function calculate_upcoming_events_count() : int {
global $wpdb;
$today = current_time( 'mysql' );
// Query using post_author and meta data
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_author = %d
AND p.post_status IN ('publish', 'future')
AND pm.meta_key = '_EventStartDate'
AND pm.meta_value >= %s",
Tribe__Events__Main::POSTTYPE,
$this->user_id,
$today
) );
return (int) $count;
}
/**
* Calculate past events count
*
* @return int
*/
private function calculate_past_events_count() : int {
global $wpdb;
$today = current_time( 'mysql' );
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_author = %d
AND p.post_status IN ('publish', 'private')
AND pm.meta_key = '_EventEndDate'
AND pm.meta_value < %s",
Tribe__Events__Main::POSTTYPE,
$this->user_id,
$today
) );
return (int) $count;
}
/**
* Calculate total tickets sold (optimized with single query)
*
* @return int
*/
private function calculate_total_tickets_sold() : int {
global $wpdb;
// Get all event IDs in one query
$event_ids = $wpdb->get_col( $wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type = %s
AND post_author = %d
AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')",
Tribe__Events__Main::POSTTYPE,
$this->user_id
) );
if ( empty( $event_ids ) ) {
return 0;
}
// Get sum of tickets sold in one query
$placeholders = array_fill( 0, count( $event_ids ), '%d' );
$sql = $wpdb->prepare(
"SELECT SUM(meta_value)
FROM {$wpdb->postmeta}
WHERE meta_key = '_tribe_tickets_sold'
AND post_id IN (" . implode( ',', $placeholders ) . ")",
$event_ids
);
$total = $wpdb->get_var( $sql );
return (int) $total;
}
/**
* Calculate total revenue (optimized)
*
* @return float
*/
private function calculate_total_revenue() : float {
global $wpdb;
// Get all event IDs in one query
$event_ids = $wpdb->get_col( $wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type = %s
AND post_author = %d
AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')",
Tribe__Events__Main::POSTTYPE,
$this->user_id
) );
if ( empty( $event_ids ) ) {
return 0.0;
}
// Get sum of revenue in one query
$placeholders = array_fill( 0, count( $event_ids ), '%d' );
$sql = $wpdb->prepare(
"SELECT SUM(meta_value)
FROM {$wpdb->postmeta}
WHERE meta_key = '_tribe_revenue_total'
AND post_id IN (" . implode( ',', $placeholders ) . ")",
$event_ids
);
$total = $wpdb->get_var( $sql );
return (float) $total;
}
/**
* Get annual revenue target
*
* @return float|null
*/
private function get_annual_revenue_target() : ?float {
$target = get_user_meta( $this->user_id, 'annual_revenue_target', true );
return ! empty( $target ) && is_numeric( $target ) ? (float) $target : null;
}
/**
* Get events table data (optimized)
*
* @param string $filter_status Status filter
* @return array
*/
public function get_events_table_data( string $filter_status = 'all' ) : array {
global $wpdb;
$valid_statuses = array( 'publish', 'future', 'draft', 'pending', 'private' );
$post_status = ( 'all' === $filter_status || ! in_array( $filter_status, $valid_statuses, true ) )
? $valid_statuses
: array( $filter_status );
// Convert to SQL-safe string
$status_placeholders = array_fill( 0, count( $post_status ), '%s' );
$status_sql = implode( ',', $status_placeholders );
// Get all events with their metadata in fewer queries
$sql = $wpdb->prepare(
"SELECT p.ID, p.post_title, p.post_status, p.guid,
MAX(CASE WHEN pm.meta_key = '_EventStartDate' THEN pm.meta_value END) as start_date,
MAX(CASE WHEN pm.meta_key = '_EventOrganizerID' THEN pm.meta_value END) as organizer_id,
MAX(CASE WHEN pm.meta_key = '_tribe_tickets_sold' THEN pm.meta_value END) as tickets_sold,
MAX(CASE WHEN pm.meta_key = '_tribe_revenue_total' THEN pm.meta_value END) as revenue
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_author = %d
AND p.post_status IN ($status_sql)
GROUP BY p.ID
ORDER BY start_date DESC",
array_merge(
array( Tribe__Events__Main::POSTTYPE, $this->user_id ),
$post_status
)
);
$events = $wpdb->get_results( $sql );
$events_data = array();
foreach ( $events as $event ) {
// Get ticket capacity
$capacity = $this->get_event_capacity( $event->ID );
$events_data[] = array(
'id' => $event->ID,
'status' => $event->post_status,
'name' => $event->post_title,
'link' => get_permalink( $event->ID ),
'start_date_ts' => strtotime( $event->start_date ),
'organizer_id' => (int) $event->organizer_id,
'capacity' => $capacity,
'sold' => (int) $event->tickets_sold,
'revenue' => (float) $event->revenue,
);
}
return $events_data;
}
/**
* Get event capacity
*
* @param int $event_id Event ID
* @return string|int
*/
private function get_event_capacity( $event_id ) {
if ( ! function_exists( 'tribe_get_tickets' ) ) {
return 0;
}
$tickets = tribe_get_tickets( $event_id );
$total_capacity = 0;
foreach ( $tickets as $ticket ) {
$capacity = $ticket->capacity();
if ( $capacity === -1 ) {
return 'Unlimited';
}
if ( is_numeric( $capacity ) ) {
$total_capacity += $capacity;
}
}
return $total_capacity;
}
}

View file

@ -0,0 +1,394 @@
<?php
/**
* HVAC Dashboard Handler
*
* Handles dashboard page rendering and functionality
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Dashboard {
/**
* Constructor
*/
public function __construct() {
add_action('init', array($this, 'register_shortcode'));
// Use higher priority to run after shortcode processing
add_filter('the_content', array($this, 'render_dashboard_content'), 99);
add_action('wp_enqueue_scripts', array($this, 'enqueue_dashboard_styles'));
}
/**
* Register dashboard shortcode
*/
public function register_shortcode() {
add_shortcode('hvac_trainer_dashboard', array($this, 'render_dashboard_shortcode'));
}
/**
* Render dashboard via shortcode
*/
public function render_dashboard_shortcode($atts) {
// Check if user is logged in and has proper permissions
if (!is_user_logged_in()) {
return '<div class="hvac-login-notice">
<p>Please log in to view the dashboard.</p>
<p><a href="' . esc_url(home_url('/community-login/')) . '" class="button">Login</a></p>
</div>';
}
if (!current_user_can('view_hvac_dashboard')) {
return '<div class="hvac-access-denied">
<p>You do not have permission to view this dashboard.</p>
</div>';
}
return $this->get_dashboard_content();
}
/**
* Render dashboard content for the page
*/
public function render_dashboard_content($content) {
// Only process if content contains our shortcode
if (has_shortcode($content, 'hvac_trainer_dashboard')) {
return do_shortcode($content);
}
return $content;
}
/**
* Get dashboard content
*/
private function get_dashboard_content() {
$user_id = get_current_user_id();
// Include dashboard data class
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-dashboard-data.php';
$dashboard_data = new HVAC_Dashboard_Data($user_id);
// Get data
$data = array(
'total_events' => $dashboard_data->get_total_events_count(),
'upcoming_events' => $dashboard_data->get_upcoming_events_count(),
'past_events' => $dashboard_data->get_past_events_count(),
'total_sold' => $dashboard_data->get_total_tickets_sold(),
'total_revenue' => $dashboard_data->get_total_revenue(),
'revenue_target' => $dashboard_data->get_annual_revenue_target(),
'events_table' => $dashboard_data->get_events_table_data(isset($_GET['event_status']) ? sanitize_key($_GET['event_status']) : 'all'),
'current_filter' => isset($_GET['event_status']) ? sanitize_key($_GET['event_status']) : 'all'
);
// Get dashboard HTML
ob_start();
?>
<div class="hvac-dashboard-wrapper">
<!-- Dashboard Header & Navigation -->
<div class="hvac-dashboard-header">
<h1>Trainer Dashboard</h1>
<div class="hvac-dashboard-nav">
<a href="<?php echo esc_url(home_url('/manage-event/')); ?>" class="button hvac-button hvac-button-primary">Create Event</a>
<a href="<?php echo esc_url(home_url('/my-events/')); ?>" class="button hvac-button hvac-button-primary">My Events</a>
<a href="<?php echo esc_url(home_url('/trainer-profile/')); ?>" class="button hvac-button hvac-button-secondary">View Profile</a>
<a href="<?php echo esc_url(wp_logout_url(home_url('/community-login/'))); ?>" class="button hvac-button hvac-button-secondary">Logout</a>
</div>
</div>
<!-- Statistics Section -->
<section class="hvac-dashboard-stats">
<h2>Your Stats</h2>
<div class="hvac-stats-grid">
<!-- Total Events -->
<div class="hvac-stat-card">
<h3>Total Events</h3>
<p class="metric-value"><?php echo esc_html($data['total_events']); ?></p>
</div>
<!-- Upcoming Events -->
<div class="hvac-stat-card">
<h3>Upcoming Events</h3>
<p class="metric-value"><?php echo esc_html($data['upcoming_events']); ?></p>
</div>
<!-- Past Events -->
<div class="hvac-stat-card">
<h3>Past Events</h3>
<p class="metric-value"><?php echo esc_html($data['past_events']); ?></p>
</div>
<!-- Total Tickets Sold -->
<div class="hvac-stat-card">
<h3>Tickets Sold</h3>
<p class="metric-value"><?php echo esc_html($data['total_sold']); ?></p>
</div>
<!-- Total Revenue -->
<div class="hvac-stat-card">
<h3>Total Revenue</h3>
<p class="metric-value">$<?php echo esc_html(number_format($data['total_revenue'], 2)); ?></p>
<?php if ($data['revenue_target']) : ?>
<small>Target: $<?php echo esc_html(number_format($data['revenue_target'], 2)); ?></small>
<?php endif; ?>
</div>
</div>
</section>
<!-- Events Table Section -->
<section class="hvac-dashboard-events">
<h2>Your Events</h2>
<!-- Tab Filters -->
<div class="hvac-event-filters">
<span>Filter: </span>
<?php
$dashboard_url = get_permalink();
$filter_statuses = array('all', 'publish', 'draft', 'pending', 'private');
foreach ($filter_statuses as $status) :
$url = ($status === 'all') ? remove_query_arg('event_status', $dashboard_url) : add_query_arg('event_status', $status, $dashboard_url);
$class = ($status === $data['current_filter']) ? 'hvac-filter-active' : '';
?>
<a href="<?php echo esc_url($url); ?>" class="hvac-filter <?php echo esc_attr($class); ?>"><?php echo esc_html(ucfirst($status)); ?></a>
<?php endforeach; ?>
</div>
<!-- Events Table -->
<div class="hvac-events-table-wrapper">
<?php if (!empty($data['events_table'])) : ?>
<table class="hvac-events-table">
<thead>
<tr>
<th>Status</th>
<th>Event Name</th>
<th>Date</th>
<th>Organizer</th>
<th>Capacity</th>
<th>Sold</th>
<th>Revenue</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($data['events_table'] as $event) : ?>
<tr>
<td><?php echo esc_html(ucfirst($event['status'])); ?></td>
<td>
<strong><a href="<?php echo esc_url($event['link']); ?>" target="_blank"><?php echo esc_html($event['name']); ?></a></strong>
</td>
<td><?php echo esc_html(date('Y-m-d H:i', $event['start_date_ts'])); ?></td>
<td><?php
if (function_exists('tribe_get_organizer')) {
echo esc_html(tribe_get_organizer($event['organizer_id']));
} else {
echo 'Organizer ID: ' . esc_html($event['organizer_id']);
}
?></td>
<td><?php echo esc_html($event['capacity']); ?></td>
<td><?php echo esc_html($event['sold']); ?></td>
<td>$<?php echo esc_html(number_format($event['revenue'], 2)); ?></td>
<td>
<?php
$edit_url = add_query_arg('event_id', $event['id'], home_url('/manage-event/'));
$summary_url = get_permalink($event['id']);
?>
<a href="<?php echo esc_url($edit_url); ?>">Edit</a> |
<a href="<?php echo esc_url($summary_url); ?>">Summary</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p>No events found.</p>
<?php endif; ?>
</div>
</section>
</div>
<?php
return ob_get_clean();
}
/**
* Enqueue dashboard styles
*/
public function enqueue_dashboard_styles() {
if (!is_page('hvac-dashboard')) {
return;
}
// Inline CSS for now - can be moved to external file later
$css = '
.hvac-dashboard-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.hvac-dashboard-header {
margin-bottom: 30px;
}
.hvac-dashboard-header h1 {
margin-bottom: 20px;
}
.hvac-dashboard-nav {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.hvac-button {
display: inline-block;
padding: 10px 20px;
text-decoration: none;
border-radius: 4px;
transition: all 0.3s ease;
}
.hvac-button-primary {
background-color: #E9AF28;
color: #000;
}
.hvac-button-primary:hover {
background-color: #d49b20;
}
.hvac-button-secondary {
background-color: #0B5C7D;
color: #fff;
}
.hvac-button-secondary:hover {
background-color: #084562;
}
.hvac-dashboard-stats {
margin-bottom: 40px;
}
.hvac-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.hvac-stat-card {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.hvac-stat-card h3 {
margin: 0 0 10px;
font-size: 16px;
color: #666;
}
.hvac-stat-card .metric-value {
font-size: 32px;
font-weight: bold;
color: #E9AF28;
margin: 0;
}
.hvac-stat-card small {
display: block;
margin-top: 5px;
color: #666;
}
.hvac-dashboard-events {
margin-top: 40px;
}
.hvac-event-filters {
margin: 20px 0;
display: flex;
gap: 10px;
align-items: center;
}
.hvac-filter {
padding: 5px 15px;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #333;
transition: all 0.3s ease;
}
.hvac-filter:hover,
.hvac-filter-active {
background-color: #E9AF28;
color: #000;
border-color: #E9AF28;
}
.hvac-events-table-wrapper {
overflow-x: auto;
}
.hvac-events-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.hvac-events-table th,
.hvac-events-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.hvac-events-table th {
background-color: #f8f9fa;
font-weight: bold;
}
.hvac-events-table tr:hover {
background-color: #f8f9fa;
}
.hvac-events-table a {
color: #0B5C7D;
text-decoration: none;
}
.hvac-events-table a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.hvac-stats-grid {
grid-template-columns: 1fr;
}
.hvac-events-table {
font-size: 14px;
}
.hvac-events-table th,
.hvac-events-table td {
padding: 8px;
}
}
';
wp_add_inline_style('astra-theme-css', $css);
}
}
// Initialize the dashboard
new HVAC_Dashboard();

View file

@ -0,0 +1,501 @@
<?php
/**
* HVAC Community Events Form Builder
*
* Helper class for building forms with proper validation and security
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 1.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Form_Builder
*/
class HVAC_Form_Builder {
/**
* Form fields configuration
*
* @var array
*/
private $fields = array();
/**
* Form attributes
*
* @var array
*/
private $form_attrs = array();
/**
* Form errors
*
* @var array
*/
private $errors = array();
/**
* Form data
*
* @var array
*/
private $data = array();
/**
* Nonce action
*
* @var string
*/
private $nonce_action;
/**
* Constructor
*
* @param string $nonce_action Nonce action for the form
*/
public function __construct( $nonce_action ) {
$this->nonce_action = $nonce_action;
$this->set_default_attributes();
}
/**
* Set default form attributes
*/
private function set_default_attributes() {
$this->form_attrs = array(
'method' => 'post',
'action' => '',
'id' => '',
'class' => 'hvac-form',
'enctype' => 'application/x-www-form-urlencoded',
);
}
/**
* Set form attributes
*
* @param array $attrs Form attributes
* @return self
*/
public function set_attributes( $attrs ) {
$this->form_attrs = array_merge( $this->form_attrs, $attrs );
return $this;
}
/**
* Add a field to the form
*
* @param array $field Field configuration
* @return self
*/
public function add_field( $field ) {
$defaults = array(
'type' => 'text',
'name' => '',
'label' => '',
'value' => '',
'required' => false,
'placeholder' => '',
'class' => '',
'id' => '',
'options' => array(),
'sanitize' => 'text',
'validate' => array(),
'description' => '',
'wrapper_class' => 'form-row',
);
$field = wp_parse_args( $field, $defaults );
// Auto-generate ID if not provided
if ( empty( $field['id'] ) && ! empty( $field['name'] ) ) {
$field['id'] = sanitize_html_class( $field['name'] );
}
$this->fields[] = $field;
return $this;
}
/**
* Set form data
*
* @param array $data Form data
* @return self
*/
public function set_data( $data ) {
$this->data = $data;
return $this;
}
/**
* Set form errors
*
* @param array $errors Form errors
* @return self
*/
public function set_errors( $errors ) {
$this->errors = $errors;
return $this;
}
/**
* Render the form
*
* @return string
*/
public function render() {
ob_start();
?>
<form <?php echo $this->get_form_attributes(); ?>>
<?php wp_nonce_field( $this->nonce_action, $this->nonce_action . '_nonce' ); ?>
<?php foreach ( $this->fields as $field ) : ?>
<?php echo $this->render_field( $field ); ?>
<?php endforeach; ?>
<div class="form-submit">
<button type="submit" class="button button-primary">Submit</button>
</div>
</form>
<?php
return ob_get_clean();
}
/**
* Get form attributes string
*
* @return string
*/
private function get_form_attributes() {
$attrs = array();
foreach ( $this->form_attrs as $key => $value ) {
if ( ! empty( $value ) ) {
$attrs[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) );
}
}
return implode( ' ', $attrs );
}
/**
* Render a single field
*
* @param array $field Field configuration
* @return string
*/
private function render_field( $field ) {
$output = sprintf( '<div class="%s">', esc_attr( $field['wrapper_class'] ) );
// Label
if ( ! empty( $field['label'] ) ) {
$output .= sprintf(
'<label for="%s">%s%s</label>',
esc_attr( $field['id'] ),
esc_html( $field['label'] ),
$field['required'] ? ' <span class="required">*</span>' : ''
);
}
// Field
switch ( $field['type'] ) {
case 'select':
$output .= $this->render_select( $field );
break;
case 'textarea':
$output .= $this->render_textarea( $field );
break;
case 'checkbox':
$output .= $this->render_checkbox( $field );
break;
case 'radio':
$output .= $this->render_radio( $field );
break;
case 'file':
$output .= $this->render_file( $field );
break;
default:
$output .= $this->render_input( $field );
}
// Description
if ( ! empty( $field['description'] ) ) {
$output .= sprintf( '<small class="description">%s</small>', esc_html( $field['description'] ) );
}
// Error
if ( isset( $this->errors[ $field['name'] ] ) ) {
$output .= sprintf(
'<span class="error">%s</span>',
esc_html( $this->errors[ $field['name'] ] )
);
}
$output .= '</div>';
return $output;
}
/**
* Render input field
*
* @param array $field Field configuration
* @return string
*/
private function render_input( $field ) {
$value = $this->get_field_value( $field['name'], $field['value'] );
return sprintf(
'<input type="%s" name="%s" id="%s" value="%s" class="%s" %s %s />',
esc_attr( $field['type'] ),
esc_attr( $field['name'] ),
esc_attr( $field['id'] ),
esc_attr( $value ),
esc_attr( $field['class'] ),
$field['required'] ? 'required' : '',
! empty( $field['placeholder'] ) ? 'placeholder="' . esc_attr( $field['placeholder'] ) . '"' : ''
);
}
/**
* Render select field
*
* @param array $field Field configuration
* @return string
*/
private function render_select( $field ) {
$value = $this->get_field_value( $field['name'], $field['value'] );
$output = sprintf(
'<select name="%s" id="%s" class="%s" %s>',
esc_attr( $field['name'] ),
esc_attr( $field['id'] ),
esc_attr( $field['class'] ),
$field['required'] ? 'required' : ''
);
foreach ( $field['options'] as $option_value => $option_label ) {
$output .= sprintf(
'<option value="%s" %s>%s</option>',
esc_attr( $option_value ),
selected( $value, $option_value, false ),
esc_html( $option_label )
);
}
$output .= '</select>';
return $output;
}
/**
* Render textarea field
*
* @param array $field Field configuration
* @return string
*/
private function render_textarea( $field ) {
$value = $this->get_field_value( $field['name'], $field['value'] );
return sprintf(
'<textarea name="%s" id="%s" class="%s" %s %s>%s</textarea>',
esc_attr( $field['name'] ),
esc_attr( $field['id'] ),
esc_attr( $field['class'] ),
$field['required'] ? 'required' : '',
! empty( $field['placeholder'] ) ? 'placeholder="' . esc_attr( $field['placeholder'] ) . '"' : '',
esc_textarea( $value )
);
}
/**
* Render checkbox field
*
* @param array $field Field configuration
* @return string
*/
private function render_checkbox( $field ) {
$value = $this->get_field_value( $field['name'], $field['value'] );
$is_checked = ! empty( $value );
return sprintf(
'<input type="checkbox" name="%s" id="%s" value="1" class="%s" %s />',
esc_attr( $field['name'] ),
esc_attr( $field['id'] ),
esc_attr( $field['class'] ),
checked( $is_checked, true, false )
);
}
/**
* Render radio field group
*
* @param array $field Field configuration
* @return string
*/
private function render_radio( $field ) {
$value = $this->get_field_value( $field['name'], $field['value'] );
$output = '<div class="radio-group">';
foreach ( $field['options'] as $option_value => $option_label ) {
$output .= sprintf(
'<label><input type="radio" name="%s" value="%s" %s /> %s</label>',
esc_attr( $field['name'] ),
esc_attr( $option_value ),
checked( $value, $option_value, false ),
esc_html( $option_label )
);
}
$output .= '</div>';
return $output;
}
/**
* Render file field
*
* @param array $field Field configuration
* @return string
*/
private function render_file( $field ) {
// Ensure form has proper enctype
$this->form_attrs['enctype'] = 'multipart/form-data';
return sprintf(
'<input type="file" name="%s" id="%s" class="%s" %s />',
esc_attr( $field['name'] ),
esc_attr( $field['id'] ),
esc_attr( $field['class'] ),
$field['required'] ? 'required' : ''
);
}
/**
* Get field value from data or default
*
* @param string $name Field name
* @param mixed $default Default value
* @return mixed
*/
private function get_field_value( $name, $default = '' ) {
return isset( $this->data[ $name ] ) ? $this->data[ $name ] : $default;
}
/**
* Validate form data
*
* @param array $data Form data to validate
* @return array Validation errors
*/
public function validate( $data ) {
$errors = array();
foreach ( $this->fields as $field ) {
$value = isset( $data[ $field['name'] ] ) ? $data[ $field['name'] ] : '';
// Required field check
if ( $field['required'] && empty( $value ) ) {
$errors[ $field['name'] ] = sprintf( '%s is required.', $field['label'] );
continue;
}
// Custom validation rules
if ( ! empty( $field['validate'] ) && ! empty( $value ) ) {
foreach ( $field['validate'] as $rule => $params ) {
$error = $this->apply_validation_rule( $value, $rule, $params, $field );
if ( $error ) {
$errors[ $field['name'] ] = $error;
break;
}
}
}
}
return $errors;
}
/**
* Apply validation rule
*
* @param mixed $value Value to validate
* @param string $rule Validation rule
* @param mixed $params Rule parameters
* @param array $field Field configuration
* @return string|false Error message or false if valid
*/
private function apply_validation_rule( $value, $rule, $params, $field ) {
switch ( $rule ) {
case 'email':
if ( ! is_email( $value ) ) {
return sprintf( '%s must be a valid email address.', $field['label'] );
}
break;
case 'url':
if ( ! filter_var( $value, FILTER_VALIDATE_URL ) ) {
return sprintf( '%s must be a valid URL.', $field['label'] );
}
break;
case 'min_length':
if ( strlen( $value ) < $params ) {
return sprintf( '%s must be at least %d characters long.', $field['label'], $params );
}
break;
case 'max_length':
if ( strlen( $value ) > $params ) {
return sprintf( '%s must not exceed %d characters.', $field['label'], $params );
}
break;
case 'pattern':
if ( ! preg_match( $params, $value ) ) {
return sprintf( '%s has an invalid format.', $field['label'] );
}
break;
}
return false;
}
/**
* Sanitize form data
*
* @param array $data Raw form data
* @return array Sanitized data
*/
public function sanitize( $data ) {
$sanitized = array();
foreach ( $this->fields as $field ) {
if ( ! isset( $data[ $field['name'] ] ) ) {
continue;
}
$value = $data[ $field['name'] ];
switch ( $field['sanitize'] ) {
case 'email':
$sanitized[ $field['name'] ] = sanitize_email( $value );
break;
case 'url':
$sanitized[ $field['name'] ] = esc_url_raw( $value );
break;
case 'textarea':
$sanitized[ $field['name'] ] = sanitize_textarea_field( $value );
break;
case 'int':
$sanitized[ $field['name'] ] = intval( $value );
break;
case 'float':
$sanitized[ $field['name'] ] = floatval( $value );
break;
case 'none':
$sanitized[ $field['name'] ] = $value;
break;
default:
$sanitized[ $field['name'] ] = sanitize_text_field( $value );
}
}
return $sanitized;
}
}

View file

@ -0,0 +1,156 @@
<?php
/**
* HVAC Community Events Logger
*
* Centralized logging system for the plugin
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 1.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Logger
*
* Handles all debug logging for the plugin
*/
class HVAC_Logger {
/**
* Whether logging is enabled
*
* @var bool
*/
private static $enabled = null;
/**
* Log prefix for all messages
*
* @var string
*/
private static $prefix = '[HVAC CE]';
/**
* Initialize the logger
*
* @return void
*/
public static function init() {
if ( null === self::$enabled ) {
self::$enabled = self::is_logging_enabled();
}
}
/**
* Check if logging is enabled
*
* @return bool
*/
private static function is_logging_enabled() {
// Check for WP_DEBUG constant
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
return true;
}
// Check for plugin-specific debug option
$plugin_debug = get_option( 'hvac_ce_debug_mode', false );
// Check for query parameter (for temporary debugging)
if ( isset( $_GET['hvac_debug'] ) && wp_verify_nonce( $_GET['hvac_debug'], 'hvac_debug_nonce' ) ) {
return true;
}
return (bool) $plugin_debug;
}
/**
* Log a debug message
*
* @param string $message The message to log
* @param string $context Optional context/category
* @param array $data Optional data to include
* @return void
*/
public static function log( $message, $context = '', $data = array() ) {
self::init();
if ( ! self::$enabled ) {
return;
}
$log_message = self::$prefix;
if ( ! empty( $context ) ) {
$log_message .= " [{$context}]";
}
$log_message .= " {$message}";
if ( ! empty( $data ) ) {
$log_message .= ' | Data: ' . print_r( $data, true );
}
// Use WordPress error_log function
error_log( $log_message );
}
/**
* Log an error
*
* @param string $message The error message
* @param string $context Optional context
* @param array $data Optional error data
* @return void
*/
public static function error( $message, $context = '', $data = array() ) {
self::log( "[ERROR] {$message}", $context, $data );
}
/**
* Log a warning
*
* @param string $message The warning message
* @param string $context Optional context
* @param array $data Optional warning data
* @return void
*/
public static function warning( $message, $context = '', $data = array() ) {
self::log( "[WARNING] {$message}", $context, $data );
}
/**
* Log an info message
*
* @param string $message The info message
* @param string $context Optional context
* @param array $data Optional data
* @return void
*/
public static function info( $message, $context = '', $data = array() ) {
self::log( "[INFO] {$message}", $context, $data );
}
/**
* Enable or disable logging
*
* @param bool $enabled Whether to enable logging
* @return void
*/
public static function set_enabled( $enabled ) {
self::$enabled = (bool) $enabled;
update_option( 'hvac_ce_debug_mode', self::$enabled );
}
/**
* Get a debug nonce for temporary debugging
*
* @return string
*/
public static function get_debug_nonce() {
return wp_create_nonce( 'hvac_debug_nonce' );
}
}

View file

@ -0,0 +1,231 @@
<?php
/**
* HVAC Community Events Security Helper
*
* Provides security utilities and validation methods
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 1.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Security
*/
class HVAC_Security {
/**
* Verify a nonce with proper error handling
*
* @param string $nonce The nonce to verify
* @param string $action The nonce action
* @param bool $die_on_fail Whether to die on failure
* @return bool
*/
public static function verify_nonce( $nonce, $action, $die_on_fail = false ) {
$is_valid = wp_verify_nonce( $nonce, $action );
if ( ! $is_valid ) {
HVAC_Logger::warning( 'Nonce verification failed', 'Security', array(
'action' => $action,
'user_id' => get_current_user_id(),
) );
if ( $die_on_fail ) {
wp_die( __( 'Security check failed. Please refresh the page and try again.', 'hvac-community-events' ) );
}
}
return $is_valid;
}
/**
* Check if current user has required capability
*
* @param string $capability The capability to check
* @param bool $die_on_fail Whether to die on failure
* @return bool
*/
public static function check_capability( $capability, $die_on_fail = false ) {
$has_cap = current_user_can( $capability );
if ( ! $has_cap ) {
HVAC_Logger::warning( 'Capability check failed', 'Security', array(
'capability' => $capability,
'user_id' => get_current_user_id(),
) );
if ( $die_on_fail ) {
wp_die( __( 'You do not have permission to perform this action.', 'hvac-community-events' ) );
}
}
return $has_cap;
}
/**
* Check if user is logged in with optional redirect
*
* @param string $redirect_to URL to redirect if not logged in
* @return bool
*/
public static function require_login( $redirect_to = '' ) {
if ( ! is_user_logged_in() ) {
if ( ! empty( $redirect_to ) ) {
wp_safe_redirect( $redirect_to );
exit;
}
return false;
}
return true;
}
/**
* Sanitize and validate email
*
* @param string $email Email to validate
* @return string|false Sanitized email or false if invalid
*/
public static function sanitize_email( $email ) {
$email = sanitize_email( $email );
return is_email( $email ) ? $email : false;
}
/**
* Sanitize and validate URL
*
* @param string $url URL to validate
* @return string|false Sanitized URL or false if invalid
*/
public static function sanitize_url( $url ) {
$url = esc_url_raw( $url );
return filter_var( $url, FILTER_VALIDATE_URL ) ? $url : false;
}
/**
* Sanitize array of values
*
* @param array $array Array to sanitize
* @param string $type Type of sanitization (text|email|url|int)
* @return array
*/
public static function sanitize_array( $array, $type = 'text' ) {
if ( ! is_array( $array ) ) {
return array();
}
$sanitized = array();
foreach ( $array as $key => $value ) {
switch ( $type ) {
case 'email':
$clean = self::sanitize_email( $value );
if ( $clean ) {
$sanitized[ $key ] = $clean;
}
break;
case 'url':
$clean = self::sanitize_url( $value );
if ( $clean ) {
$sanitized[ $key ] = $clean;
}
break;
case 'int':
$sanitized[ $key ] = intval( $value );
break;
default:
$sanitized[ $key ] = sanitize_text_field( $value );
}
}
return $sanitized;
}
/**
* Escape output based on context
*
* @param mixed $value Value to escape
* @param string $context Context (html|attr|url|js)
* @return string
*/
public static function escape_output( $value, $context = 'html' ) {
switch ( $context ) {
case 'attr':
return esc_attr( $value );
case 'url':
return esc_url( $value );
case 'js':
return esc_js( $value );
case 'textarea':
return esc_textarea( $value );
default:
return esc_html( $value );
}
}
/**
* Check if request is AJAX
*
* @return bool
*/
public static function is_ajax_request() {
return defined( 'DOING_AJAX' ) && DOING_AJAX;
}
/**
* Get user IP address
*
* @return string
*/
public static function get_user_ip() {
$ip = '';
if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
$ip = $_SERVER['REMOTE_ADDR'];
}
return sanitize_text_field( $ip );
}
/**
* Rate limiting check
*
* @param string $action Action to limit
* @param int $limit Number of attempts allowed
* @param int $window Time window in seconds
* @param string $identifier User identifier (defaults to IP)
* @return bool True if within limits, false if exceeded
*/
public static function check_rate_limit( $action, $limit = 5, $window = 300, $identifier = null ) {
if ( null === $identifier ) {
$identifier = self::get_user_ip();
}
$key = 'hvac_rate_limit_' . md5( $action . $identifier );
$attempts = get_transient( $key );
if ( false === $attempts ) {
set_transient( $key, 1, $window );
return true;
}
if ( $attempts >= $limit ) {
HVAC_Logger::warning( 'Rate limit exceeded', 'Security', array(
'action' => $action,
'identifier' => $identifier,
'attempts' => $attempts,
) );
return false;
}
set_transient( $key, $attempts + 1, $window );
return true;
}
}

View file

@ -0,0 +1,411 @@
<?php
/**
* HVAC Community Events Settings - Refactored
*
* Handles plugin settings and configuration
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 1.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Settings_Refactored
*/
class HVAC_Settings_Refactored {
/**
* Settings option name
*
* @var string
*/
private $option_name = 'hvac_ce_settings';
/**
* Settings page slug
*
* @var string
*/
private $page_slug = 'hvac-community-events';
/**
* Settings group
*
* @var string
*/
private $settings_group = 'hvac_ce_settings_group';
/**
* Default settings
*
* @var array
*/
private $defaults = array();
/**
* Cached settings
*
* @var array
*/
private $settings = null;
/**
* Constructor
*/
public function __construct() {
$this->set_defaults();
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
}
/**
* Set default settings
*/
private function set_defaults() {
$this->defaults = array(
'general' => array(
'debug_mode' => false,
'cache_duration' => 300,
'enable_notifications' => true,
),
'registration' => array(
'auto_approve' => false,
'require_venue' => false,
'email_verification' => true,
'terms_url' => '',
),
'dashboard' => array(
'items_per_page' => 20,
'show_revenue' => true,
'show_capacity' => true,
'date_format' => 'Y-m-d',
),
'notifications' => array(
'admin_email' => get_option( 'admin_email' ),
'from_email' => get_option( 'admin_email' ),
'from_name' => get_option( 'blogname' ),
),
'advanced' => array(
'uninstall_data' => false,
'legacy_support' => false,
),
);
}
/**
* Get all settings
*
* @return array
*/
public function get_settings() {
if ( null === $this->settings ) {
$this->settings = get_option( $this->option_name, array() );
$this->settings = wp_parse_args( $this->settings, $this->defaults );
}
return $this->settings;
}
/**
* Get a specific setting
*
* @param string $section Setting section
* @param string $key Setting key
* @param mixed $default Default value if not set
* @return mixed
*/
public function get( $section, $key, $default = null ) {
$settings = $this->get_settings();
if ( isset( $settings[ $section ][ $key ] ) ) {
return $settings[ $section ][ $key ];
}
if ( null !== $default ) {
return $default;
}
return isset( $this->defaults[ $section ][ $key ] )
? $this->defaults[ $section ][ $key ]
: null;
}
/**
* Update a setting
*
* @param string $section Setting section
* @param string $key Setting key
* @param mixed $value New value
* @return bool
*/
public function update( $section, $key, $value ) {
$settings = $this->get_settings();
if ( ! isset( $settings[ $section ] ) ) {
$settings[ $section ] = array();
}
$settings[ $section ][ $key ] = $value;
$this->settings = $settings;
return update_option( $this->option_name, $settings );
}
/**
* Add settings page to admin menu
*/
public function add_settings_page() {
add_options_page(
__( 'HVAC Community Events Settings', 'hvac-community-events' ),
__( 'HVAC Events', 'hvac-community-events' ),
'manage_options',
$this->page_slug,
array( $this, 'render_settings_page' )
);
}
/**
* Register settings
*/
public function register_settings() {
register_setting(
$this->settings_group,
$this->option_name,
array( $this, 'sanitize_settings' )
);
// General Settings Section
add_settings_section(
'hvac_ce_general',
__( 'General Settings', 'hvac-community-events' ),
array( $this, 'render_section_general' ),
$this->page_slug
);
add_settings_field(
'debug_mode',
__( 'Debug Mode', 'hvac-community-events' ),
array( $this, 'render_field_checkbox' ),
$this->page_slug,
'hvac_ce_general',
array(
'label_for' => 'debug_mode',
'section' => 'general',
'key' => 'debug_mode',
'description' => __( 'Enable debug logging', 'hvac-community-events' ),
)
);
add_settings_field(
'cache_duration',
__( 'Cache Duration', 'hvac-community-events' ),
array( $this, 'render_field_number' ),
$this->page_slug,
'hvac_ce_general',
array(
'label_for' => 'cache_duration',
'section' => 'general',
'key' => 'cache_duration',
'description' => __( 'Cache duration in seconds', 'hvac-community-events' ),
'min' => 60,
'max' => 3600,
)
);
// Registration Settings Section
add_settings_section(
'hvac_ce_registration',
__( 'Registration Settings', 'hvac-community-events' ),
array( $this, 'render_section_registration' ),
$this->page_slug
);
add_settings_field(
'auto_approve',
__( 'Auto Approve', 'hvac-community-events' ),
array( $this, 'render_field_checkbox' ),
$this->page_slug,
'hvac_ce_registration',
array(
'label_for' => 'auto_approve',
'section' => 'registration',
'key' => 'auto_approve',
'description' => __( 'Automatically approve new trainer registrations', 'hvac-community-events' ),
)
);
// Add more sections and fields as needed
}
/**
* Render settings page
*/
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Show success message if settings were saved
if ( isset( $_GET['settings-updated'] ) ) {
add_settings_error(
'hvac_ce_settings',
'hvac_ce_settings_message',
__( 'Settings saved.', 'hvac-community-events' ),
'updated'
);
}
settings_errors( 'hvac_ce_settings' );
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( $this->settings_group );
do_settings_sections( $this->page_slug );
submit_button( __( 'Save Settings', 'hvac-community-events' ) );
?>
</form>
</div>
<?php
}
/**
* Render general section description
*/
public function render_section_general() {
echo '<p>' . __( 'Configure general plugin settings.', 'hvac-community-events' ) . '</p>';
}
/**
* Render registration section description
*/
public function render_section_registration() {
echo '<p>' . __( 'Configure trainer registration settings.', 'hvac-community-events' ) . '</p>';
}
/**
* Render checkbox field
*
* @param array $args Field arguments
*/
public function render_field_checkbox( $args ) {
$value = $this->get( $args['section'], $args['key'] );
?>
<input type="checkbox"
id="<?php echo esc_attr( $args['label_for'] ); ?>"
name="<?php echo esc_attr( $this->option_name ); ?>[<?php echo esc_attr( $args['section'] ); ?>][<?php echo esc_attr( $args['key'] ); ?>]"
value="1"
<?php checked( 1, $value, true ); ?>
/>
<?php if ( ! empty( $args['description'] ) ) : ?>
<p class="description"><?php echo esc_html( $args['description'] ); ?></p>
<?php endif;
}
/**
* Render number field
*
* @param array $args Field arguments
*/
public function render_field_number( $args ) {
$value = $this->get( $args['section'], $args['key'] );
?>
<input type="number"
id="<?php echo esc_attr( $args['label_for'] ); ?>"
name="<?php echo esc_attr( $this->option_name ); ?>[<?php echo esc_attr( $args['section'] ); ?>][<?php echo esc_attr( $args['key'] ); ?>]"
value="<?php echo esc_attr( $value ); ?>"
min="<?php echo esc_attr( $args['min'] ?? 0 ); ?>"
max="<?php echo esc_attr( $args['max'] ?? '' ); ?>"
class="regular-text"
/>
<?php if ( ! empty( $args['description'] ) ) : ?>
<p class="description"><?php echo esc_html( $args['description'] ); ?></p>
<?php endif;
}
/**
* Render text field
*
* @param array $args Field arguments
*/
public function render_field_text( $args ) {
$value = $this->get( $args['section'], $args['key'] );
?>
<input type="text"
id="<?php echo esc_attr( $args['label_for'] ); ?>"
name="<?php echo esc_attr( $this->option_name ); ?>[<?php echo esc_attr( $args['section'] ); ?>][<?php echo esc_attr( $args['key'] ); ?>]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
/>
<?php if ( ! empty( $args['description'] ) ) : ?>
<p class="description"><?php echo esc_html( $args['description'] ); ?></p>
<?php endif;
}
/**
* Sanitize settings
*
* @param array $input Raw input data
* @return array Sanitized data
*/
public function sanitize_settings( $input ) {
$sanitized = array();
// General settings
if ( isset( $input['general'] ) ) {
$sanitized['general'] = array(
'debug_mode' => ! empty( $input['general']['debug_mode'] ),
'cache_duration' => absint( $input['general']['cache_duration'] ?? 300 ),
'enable_notifications' => ! empty( $input['general']['enable_notifications'] ),
);
// Update debug mode in logger
HVAC_Logger::set_enabled( $sanitized['general']['debug_mode'] );
}
// Registration settings
if ( isset( $input['registration'] ) ) {
$sanitized['registration'] = array(
'auto_approve' => ! empty( $input['registration']['auto_approve'] ),
'require_venue' => ! empty( $input['registration']['require_venue'] ),
'email_verification' => ! empty( $input['registration']['email_verification'] ),
'terms_url' => esc_url_raw( $input['registration']['terms_url'] ?? '' ),
);
}
// Dashboard settings
if ( isset( $input['dashboard'] ) ) {
$sanitized['dashboard'] = array(
'items_per_page' => absint( $input['dashboard']['items_per_page'] ?? 20 ),
'show_revenue' => ! empty( $input['dashboard']['show_revenue'] ),
'show_capacity' => ! empty( $input['dashboard']['show_capacity'] ),
'date_format' => sanitize_text_field( $input['dashboard']['date_format'] ?? 'Y-m-d' ),
);
}
// Merge with existing settings to preserve sections not being updated
$existing = $this->get_settings();
$sanitized = wp_parse_args( $sanitized, $existing );
return $sanitized;
}
/**
* Get instance of settings class (singleton)
*
* @return self
*/
public static function get_instance() {
static $instance = null;
if ( null === $instance ) {
$instance = new self();
}
return $instance;
}
}

View file

@ -0,0 +1,134 @@
# Zoho CRM Integration Setup Guide
## Overview
This integration syncs WordPress Events Calendar data with Zoho CRM, mapping:
- Events → Campaigns
- Users/Trainers → Contacts
- Ticket Purchases → Invoices
## Setup Steps
### 1. Create Zoho OAuth Application
1. Go to [Zoho API Console](https://api-console.zoho.com/)
2. Click "CREATE NEW CLIENT"
3. Choose "Server-based Applications"
4. Fill in details:
- **Client Name**: HVAC Community Events Integration
- **Homepage URL**: Your WordPress site URL
- **Authorized Redirect URIs**:
- For setup script: `http://localhost:8080/callback`
- For WordPress admin: `https://your-site.com/wp-admin/edit.php?post_type=tribe_events&page=hvac-zoho-crm`
5. Click "CREATE" and save your Client ID and Client Secret
### 2. Generate Credentials Using Setup Script
The easiest way to set up credentials:
```bash
cd wp-content/plugins/hvac-community-events/includes/zoho
php setup-helper.php
```
Follow the prompts to:
1. Enter your Client ID and Client Secret
2. Open the authorization URL in your browser
3. Grant permissions and copy the authorization code
4. The script will automatically:
- Exchange the code for tokens
- Get your organization ID
- Create the `zoho-config.php` file
### 3. Alternative: Manual Setup
If you prefer manual setup:
1. Create `zoho-config.php` from the template:
```bash
cp zoho-config-template.php zoho-config.php
```
2. Generate authorization URL:
```
https://accounts.zoho.com/oauth/v2/auth?
scope=ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.users.all&
client_id=YOUR_CLIENT_ID&
response_type=code&
access_type=offline&
redirect_uri=http://localhost:8080/callback
```
3. Exchange code for tokens using cURL:
```bash
curl -X POST https://accounts.zoho.com/oauth/v2/token \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "redirect_uri=http://localhost:8080/callback" \
-d "code=YOUR_AUTH_CODE"
```
4. Get organization ID:
```bash
curl -X GET https://www.zohoapis.com/crm/v2/org \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN"
```
5. Update `zoho-config.php` with your credentials
### 4. WordPress Admin Setup
After creating the config file:
1. Go to WordPress Admin → Events → Zoho CRM
2. The integration will automatically detect your configuration
3. Click "Test Connection" to verify
4. Click "Create Custom Fields" to set up required fields in Zoho
## Required Permissions
The integration needs these Zoho CRM scopes:
- `ZohoCRM.settings.all` - For creating custom fields
- `ZohoCRM.modules.all` - For reading/writing records
- `ZohoCRM.users.all` - For user information
- `ZohoCRM.org.all` - For organization details (optional)
## Security Notes
- **NEVER** commit `zoho-config.php` to version control
- Keep your refresh token secure
- The integration automatically handles token refresh
- All API calls are logged for debugging (disable in production)
## Troubleshooting
### Common Issues
1. **"Invalid Client" Error**
- Verify Client ID and Secret are correct
- Ensure redirect URI matches exactly
2. **"Invalid Code" Error**
- Authorization codes expire quickly (< 1 minute)
- Generate and use immediately
3. **"No Refresh Token" Error**
- Make sure `access_type=offline` in auth URL
- Include `prompt=consent` to force new refresh token
### Debug Mode
Enable debug logging in `zoho-config.php`:
```php
define('ZOHO_DEBUG_MODE', true);
define('ZOHO_LOG_FILE', WP_CONTENT_DIR . '/zoho-crm-debug.log');
```
Check the log file for detailed API responses.
## Support
For issues or questions:
1. Check the debug log
2. Verify credentials in Zoho API Console
3. Ensure all required modules are enabled in Zoho CRM

View file

@ -0,0 +1,95 @@
# Zoho CRM Integration - Staging Mode
## Overview
The Zoho CRM integration has a built-in staging mode to prevent accidental data synchronization from development or staging environments to the production Zoho CRM database.
## How It Works
### Domain Detection
- **Production Domain**: `upskillhvac.com`
- **Staging Domains**: All other domains (e.g., `*.cloudwaysapps.com`)
### Staging Mode Behavior
When running on any domain other than `upskillhvac.com`:
1. **Read Operations**: Allowed (GET requests)
2. **Write Operations**: Blocked (POST, PUT, DELETE, PATCH requests)
3. **Visual Indicators**: Admin interface shows "STAGING MODE ACTIVE" banner
4. **Sync Simulation**: Shows what data would be synced without actually sending it
### Production Mode
When running on `upskillhvac.com`:
- All operations are allowed
- Data syncs directly to Zoho CRM
- No staging mode indicators
## Admin Interface
### Staging Mode Indicators
- Blue info banner at the top of the Zoho CRM Sync page
- Shows current site URL
- Displays "STAGING MODE - Simulation Results" on sync operations
### Test Data Preview
In staging mode, sync operations return:
- Total records that would be synced
- Detailed preview of first 5 records
- Field mappings that would be used
## Implementation Details
### Class: `HVAC_Zoho_Sync`
```php
private function is_sync_allowed() {
$site_url = get_site_url();
return strpos($site_url, 'upskillhvac.com') !== false;
}
```
### Class: `HVAC_Zoho_CRM_Auth`
```php
// Blocks write operations in staging mode
if ($is_staging && in_array($method, array('POST', 'PUT', 'DELETE', 'PATCH'))) {
return [simulated response];
}
```
## Testing in Staging
1. Access WordPress Admin → HVAC Community Events → Zoho CRM Sync
2. See "STAGING MODE ACTIVE" banner
3. Click sync buttons to see simulated results
4. Review test data in expandable preview sections
5. No actual data is sent to Zoho CRM
## Deploying to Production
1. Deploy code to `upskillhvac.com`
2. Staging mode automatically deactivates
3. Sync operations will write to Zoho CRM
4. Monitor first sync carefully
## Configuration
No configuration needed - staging mode is automatic based on domain detection.
## Security Benefits
- Prevents test data from polluting production CRM
- Allows safe testing of sync logic
- No configuration mistakes possible
- Clear visual indicators prevent confusion
## Troubleshooting
### Staging Mode Not Activating
- Check site URL with `get_site_url()`
- Ensure domain doesn't contain "upskillhvac.com"
### Production Sync Not Working
- Verify site URL contains "upskillhvac.com"
- Check OAuth credentials are configured
- Review error logs for API issues

View file

@ -0,0 +1,97 @@
# Testing Zoho CRM Integration
## Prerequisites
Your `.env` file now contains:
- `ZOHO_CLIENT_ID`
- `ZOHO_CLIENT_SECRET`
## Testing Process
### Option 1: Using the Test Script (Recommended)
1. Open a terminal and run:
```bash
cd /Users/ben/dev/upskill-event-manager/wordpress-dev
./bin/test-zoho-integration.sh
```
2. When prompted, choose 'y' to start the OAuth callback server
3. Open a new terminal and run the script again, choosing 'n' this time
4. Follow the prompts:
- Open the authorization URL in your browser
- Log in to Zoho and authorize the app
- Copy the authorization code from the callback page
- Paste it in the terminal
### Option 2: Manual Testing
1. Generate the authorization URL:
```
https://accounts.zoho.com/oauth/v2/auth?
scope=ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.users.all,ZohoCRM.org.all&
client_id=1000.Z0HOF1VMMJ9W2QWSU57GVQYEAVUSKS&
response_type=code&
access_type=offline&
redirect_uri=http://localhost:8080/callback&
prompt=consent
```
2. Open the URL in your browser
3. After authorization, copy the code from the redirect URL
4. Run the test script:
```bash
cd /Users/ben/dev/upskill-event-manager/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho
php test-integration.php
```
5. Paste the authorization code when prompted
## What the Test Does
1. **Validates Credentials** - Checks that your client ID and secret work
2. **Gets Tokens** - Exchanges the auth code for access and refresh tokens
3. **Fetches Org Info** - Gets your Zoho organization details
4. **Tests Module Access** - Verifies access to Campaigns, Contacts, and Invoices
5. **Creates Config File** - Saves all credentials to `zoho-config.php`
6. **Updates .env** - Adds the refresh token for future use
## Expected Output
You should see:
- ✓ Credentials loaded from .env file
- ✓ Tokens received successfully
- ✓ Organization found
- ✓ Campaigns module accessible
- ✓ Contacts module accessible
- ✓ Invoices module accessible
- ✓ Configuration file created
## Next Steps
After successful testing:
1. The system is ready for field creation
2. You can start syncing data
3. Check WordPress admin → Events → Zoho CRM for status
## Troubleshooting
### "Invalid Client" Error
- Verify the client ID and secret in your .env file
- Check that you're using the correct Zoho data center (US, EU, IN, AU)
### "Invalid Code" Error
- Authorization codes expire in ~1 minute
- Generate a new code and use it immediately
### Connection Issues
- Make sure you can access https://accounts.zoho.com
- Check if you need to use a different regional URL
### Module Access Issues
- Ensure all required modules are enabled in your Zoho CRM
- Check that your Zoho plan includes API access

View file

@ -0,0 +1,57 @@
<?php
/**
* Simple OAuth Callback Server
*
* This script creates a local server to capture the OAuth callback
* Usage: php auth-server.php
*/
echo "Starting OAuth callback server on http://localhost:8080\n";
echo "Waiting for authorization callback...\n\n";
// Start built-in PHP server
$server = stream_socket_server("tcp://127.0.0.1:8080", $errno, $errstr);
if (!$server) {
die("Error: $errstr ($errno)\n");
}
while ($conn = stream_socket_accept($server)) {
$request = fread($conn, 1024);
// Parse the request
if (preg_match('/GET \/callback\?code=([^\s&]+)/', $request, $matches)) {
$auth_code = $matches[1];
// Send response
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html\r\n\r\n";
$response .= "<html><body>";
$response .= "<h1>Authorization Successful!</h1>";
$response .= "<p>Authorization code received. You can close this window.</p>";
$response .= "<p>Code: <code>$auth_code</code></p>";
$response .= "<p>Copy this code and paste it in the terminal.</p>";
$response .= "</body></html>";
fwrite($conn, $response);
fclose($conn);
echo "Authorization code received: $auth_code\n";
echo "Copy this code to your terminal.\n";
// Keep server running to display the page
sleep(10);
break;
} else {
// Send 404 for other requests
$response = "HTTP/1.1 404 Not Found\r\n";
$response .= "Content-Type: text/html\r\n\r\n";
$response .= "<html><body><h1>404 Not Found</h1></body></html>";
fwrite($conn, $response);
fclose($conn);
}
}
fclose($server);
echo "\nServer stopped.\n";

View file

@ -0,0 +1,211 @@
<?php
/**
* Zoho CRM Admin Interface
*
* Provides WordPress admin interface for Zoho credential management
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Zoho_Admin {
public function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'handle_auth_callback'));
}
/**
* Add menu item to WordPress admin
*/
public function add_admin_menu() {
add_submenu_page(
'edit.php?post_type=tribe_events',
'Zoho CRM Integration',
'Zoho CRM',
'manage_options',
'hvac-zoho-crm',
array($this, 'admin_page')
);
}
/**
* Handle OAuth callback
*/
public function handle_auth_callback() {
if (isset($_GET['page']) && $_GET['page'] === 'hvac-zoho-crm' && isset($_GET['code'])) {
$auth = new HVAC_Zoho_CRM_Auth();
if ($auth->exchange_code_for_tokens($_GET['code'])) {
add_settings_error(
'hvac_zoho_messages',
'hvac_zoho_auth_success',
'Successfully connected to Zoho CRM!',
'success'
);
} else {
add_settings_error(
'hvac_zoho_messages',
'hvac_zoho_auth_error',
'Failed to connect to Zoho CRM. Please check your credentials.',
'error'
);
}
// Redirect to remove code from URL
wp_redirect(admin_url('edit.php?post_type=tribe_events&page=hvac-zoho-crm'));
exit;
}
}
/**
* Display admin page
*/
public function admin_page() {
?>
<div class="wrap">
<h1>Zoho CRM Integration</h1>
<?php settings_errors('hvac_zoho_messages'); ?>
<?php
// Check if config file exists
$config_file = plugin_dir_path(dirname(__FILE__)) . 'zoho/zoho-config.php';
$config_exists = file_exists($config_file);
if (!$config_exists):
?>
<div class="notice notice-warning">
<p>Zoho CRM configuration file not found. Please follow the setup instructions below.</p>
</div>
<h2>Setup Instructions</h2>
<ol>
<li>
<strong>Register your application in Zoho:</strong>
<a href="https://api-console.zoho.com/" target="_blank">Go to Zoho API Console</a>
</li>
<li>Create a new Server-based Application</li>
<li>Set redirect URI to: <code><?php echo admin_url('edit.php?post_type=tribe_events&page=hvac-zoho-crm'); ?></code></li>
<li>Copy your Client ID and Client Secret</li>
<li>Run the setup helper script from command line:
<pre>cd <?php echo plugin_dir_path(dirname(__FILE__)); ?>zoho
php setup-helper.php</pre>
</li>
</ol>
<?php else: ?>
<?php
// Load configuration
require_once $config_file;
$auth = new HVAC_Zoho_CRM_Auth();
// Test connection
$org_info = $auth->make_api_request('/crm/v2/org');
$connected = !is_wp_error($org_info) && isset($org_info['org']);
?>
<?php if ($connected): ?>
<div class="notice notice-success">
<p> Connected to Zoho CRM</p>
</div>
<h2>Organization Information</h2>
<table class="form-table">
<tr>
<th>Organization Name</th>
<td><?php echo esc_html($org_info['org'][0]['company_name']); ?></td>
</tr>
<tr>
<th>Organization ID</th>
<td><?php echo esc_html($org_info['org'][0]['id']); ?></td>
</tr>
<tr>
<th>Time Zone</th>
<td><?php echo esc_html($org_info['org'][0]['time_zone']); ?></td>
</tr>
</table>
<h2>Integration Status</h2>
<?php $this->display_integration_status(); ?>
<h2>Actions</h2>
<p>
<a href="<?php echo wp_nonce_url(admin_url('edit.php?post_type=tribe_events&page=hvac-zoho-crm&action=test_sync'), 'test_sync'); ?>"
class="button button-primary">Test Sync</a>
<a href="<?php echo wp_nonce_url(admin_url('edit.php?post_type=tribe_events&page=hvac-zoho-crm&action=create_fields'), 'create_fields'); ?>"
class="button">Create Custom Fields</a>
</p>
<?php else: ?>
<div class="notice notice-error">
<p> Not connected to Zoho CRM</p>
</div>
<h2>Reconnect to Zoho</h2>
<p>Click the button below to authorize this application with Zoho CRM:</p>
<p>
<a href="<?php echo esc_url($auth->get_authorization_url()); ?>"
class="button button-primary">Connect to Zoho CRM</a>
</p>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
}
/**
* Display integration status
*/
private function display_integration_status() {
?>
<table class="widefat striped">
<thead>
<tr>
<th>Module</th>
<th>Fields Configured</th>
<th>Last Sync</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Campaigns (Events)</td>
<td><?php echo $this->check_custom_fields('Campaigns'); ?></td>
<td><?php echo get_option('hvac_zoho_last_campaign_sync', 'Never'); ?></td>
<td><span class="dashicons dashicons-yes-alt" style="color: green;"></span></td>
</tr>
<tr>
<td>Contacts (Users)</td>
<td><?php echo $this->check_custom_fields('Contacts'); ?></td>
<td><?php echo get_option('hvac_zoho_last_contact_sync', 'Never'); ?></td>
<td><span class="dashicons dashicons-yes-alt" style="color: green;"></span></td>
</tr>
<tr>
<td>Invoices (Orders)</td>
<td><?php echo $this->check_custom_fields('Invoices'); ?></td>
<td><?php echo get_option('hvac_zoho_last_invoice_sync', 'Never'); ?></td>
<td><span class="dashicons dashicons-yes-alt" style="color: green;"></span></td>
</tr>
</tbody>
</table>
<?php
}
/**
* Check if custom fields exist
*/
private function check_custom_fields($module) {
// This would actually check via API if the custom fields exist
// For now, return a placeholder
return '<span style="color: orange;">Pending</span>';
}
}
// Initialize admin interface
if (is_admin()) {
new HVAC_Zoho_Admin();
}

View file

@ -0,0 +1,262 @@
<?php
/**
* Zoho CRM Authentication Handler
*
* Handles OAuth token management and API authentication
*
* @package HVAC_Community_Events
* @subpackage Zoho_Integration
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Zoho_CRM_Auth {
private $client_id;
private $client_secret;
private $refresh_token;
private $redirect_uri;
private $access_token;
private $token_expiry;
public function __construct() {
// Load configuration if available
$config_file = plugin_dir_path(__FILE__) . 'zoho-config.php';
if (file_exists($config_file)) {
require_once $config_file;
$this->client_id = ZOHO_CLIENT_ID;
$this->client_secret = ZOHO_CLIENT_SECRET;
$this->refresh_token = ZOHO_REFRESH_TOKEN;
$this->redirect_uri = ZOHO_REDIRECT_URI;
}
// Load stored access token from WordPress options
$this->load_access_token();
}
/**
* Generate authorization URL for initial setup
*/
public function get_authorization_url() {
$params = array(
'scope' => ZOHO_SCOPES,
'client_id' => $this->client_id,
'response_type' => 'code',
'access_type' => 'offline',
'redirect_uri' => $this->redirect_uri,
'prompt' => 'consent'
);
return ZOHO_ACCOUNTS_URL . '/oauth/v2/auth?' . http_build_query($params);
}
/**
* Exchange authorization code for tokens
*/
public function exchange_code_for_tokens($auth_code) {
$url = ZOHO_ACCOUNTS_URL . '/oauth/v2/token';
$params = array(
'grant_type' => 'authorization_code',
'client_id' => $this->client_id,
'client_secret' => $this->client_secret,
'redirect_uri' => $this->redirect_uri,
'code' => $auth_code
);
$response = wp_remote_post($url, array(
'body' => $params,
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded'
)
));
if (is_wp_error($response)) {
$this->log_error('Failed to exchange code: ' . $response->get_error_message());
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (isset($data['access_token']) && isset($data['refresh_token'])) {
$this->access_token = $data['access_token'];
$this->refresh_token = $data['refresh_token'];
$this->token_expiry = time() + $data['expires_in'];
// Save tokens
$this->save_tokens();
return true;
}
$this->log_error('Invalid token response: ' . $body);
return false;
}
/**
* Get valid access token (refresh if needed)
*/
public function get_access_token() {
// Check if token is expired or will expire soon (5 mins buffer)
if (!$this->access_token || (time() + 300) >= $this->token_expiry) {
$this->refresh_access_token();
}
return $this->access_token;
}
/**
* Refresh access token using refresh token
*/
private function refresh_access_token() {
$url = ZOHO_ACCOUNTS_URL . '/oauth/v2/token';
$params = array(
'refresh_token' => $this->refresh_token,
'client_id' => $this->client_id,
'client_secret' => $this->client_secret,
'grant_type' => 'refresh_token'
);
$response = wp_remote_post($url, array(
'body' => $params,
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded'
)
));
if (is_wp_error($response)) {
$this->log_error('Failed to refresh token: ' . $response->get_error_message());
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (isset($data['access_token'])) {
$this->access_token = $data['access_token'];
$this->token_expiry = time() + $data['expires_in'];
$this->save_access_token();
return true;
}
$this->log_error('Failed to refresh token: ' . $body);
return false;
}
/**
* Make authenticated API request
*/
public function make_api_request($endpoint, $method = 'GET', $data = null) {
// Check if we're in staging mode
$site_url = get_site_url();
$is_staging = strpos($site_url, 'upskillhvac.com') === false;
// In staging mode, only allow read operations, no writes
if ($is_staging && in_array($method, array('POST', 'PUT', 'DELETE', 'PATCH'))) {
$this->log_debug('STAGING MODE: Simulating ' . $method . ' request to ' . $endpoint);
return array(
'data' => array(
array(
'code' => 'STAGING_MODE',
'details' => array(
'message' => 'Staging mode active. Write operations are disabled.'
),
'message' => 'This would have been a ' . $method . ' request to: ' . $endpoint,
'status' => 'success'
)
)
);
}
$access_token = $this->get_access_token();
if (!$access_token) {
return new WP_Error('no_token', 'No valid access token available');
}
$url = ZOHO_API_BASE_URL . $endpoint;
$args = array(
'method' => $method,
'headers' => array(
'Authorization' => 'Zoho-oauthtoken ' . $access_token,
'Content-Type' => 'application/json'
)
);
if ($data && in_array($method, array('POST', 'PUT', 'PATCH'))) {
$args['body'] = json_encode($data);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
$this->log_error('API request failed: ' . $response->get_error_message());
return $response;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
// Log response for debugging
if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE) {
$this->log_debug('API Response: ' . $body);
}
return $data;
}
/**
* Save tokens to WordPress options
*/
private function save_tokens() {
update_option('hvac_zoho_refresh_token', $this->refresh_token);
$this->save_access_token();
}
/**
* Save access token
*/
private function save_access_token() {
update_option('hvac_zoho_access_token', $this->access_token);
update_option('hvac_zoho_token_expiry', $this->token_expiry);
}
/**
* Load access token from WordPress options
*/
private function load_access_token() {
$this->access_token = get_option('hvac_zoho_access_token');
$this->token_expiry = get_option('hvac_zoho_token_expiry', 0);
// Load refresh token if not set
if (!$this->refresh_token) {
$this->refresh_token = get_option('hvac_zoho_refresh_token');
}
}
/**
* Log error messages
*/
private function log_error($message) {
if (defined('ZOHO_LOG_FILE')) {
error_log('[' . date('Y-m-d H:i:s') . '] ERROR: ' . $message . PHP_EOL, 3, ZOHO_LOG_FILE);
}
}
/**
* Log debug messages
*/
private function log_debug($message) {
if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE && defined('ZOHO_LOG_FILE')) {
error_log('[' . date('Y-m-d H:i:s') . '] DEBUG: ' . $message . PHP_EOL, 3, ZOHO_LOG_FILE);
}
}
}

View file

@ -0,0 +1,428 @@
<?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;
/**
* 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
$site_url = get_site_url();
$this->is_staging = strpos($site_url, 'upskillhvac.com') === false;
}
/**
* Check if sync is allowed
*
* @return bool
*/
private function is_sync_allowed() {
// Only allow sync on production (upskillhvac.com)
$site_url = get_site_url();
return strpos($site_url, 'upskillhvac.com') !== false;
}
/**
* Sync events to Zoho Campaigns
*
* @return array Sync results
*/
public function sync_events() {
$results = array(
'total' => 0,
'synced' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging
);
// Get all published events
$events = tribe_get_events(array(
'posts_per_page' => -1,
'eventDisplay' => 'list',
'meta_query' => array(
array(
'key' => '_hvac_event_type',
'value' => 'trainer',
'compare' => '='
)
)
));
$results['total'] = 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 campaign already exists in Zoho
$search_response = $this->auth->make_api_request('GET', '/crm/v2/Campaigns/search', array(
'criteria' => "(Campaign_Name:equals:{$campaign_data['Campaign_Name']})"
));
if (!empty($search_response['data'])) {
// Update existing campaign
$campaign_id = $search_response['data'][0]['id'];
$update_response = $this->auth->make_api_request('PUT', "/crm/v2/Campaigns/{$campaign_id}", array(
'data' => array($campaign_data)
));
} else {
// Create new campaign
$create_response = $this->auth->make_api_request('POST', '/crm/v2/Campaigns', array(
'data' => array($campaign_data)
));
}
$results['synced']++;
// Update event meta with Zoho ID
if (isset($campaign_id)) {
update_post_meta($event->ID, '_zoho_campaign_id', $campaign_id);
}
} catch (Exception $e) {
$results['failed']++;
$results['errors'][] = sprintf('Event %s: %s', $event->ID, $e->getMessage());
}
}
return $results;
}
/**
* Sync users to Zoho Contacts
*
* @return array Sync results
*/
public function sync_users() {
$results = array(
'total' => 0,
'synced' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging
);
// Get trainers and attendees
$users = get_users(array(
'role__in' => array('trainer', 'trainee'),
'meta_query' => array(
'relation' => 'OR',
array(
'key' => '_sync_to_zoho',
'value' => '1',
'compare' => '='
),
array(
'key' => '_sync_to_zoho',
'compare' => 'NOT EXISTS'
)
)
));
$results['total'] = 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 contact already exists in Zoho
$search_response = $this->auth->make_api_request('GET', '/crm/v2/Contacts/search', 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('PUT', "/crm/v2/Contacts/{$contact_id}", array(
'data' => array($contact_data)
));
} else {
// Create new contact
$create_response = $this->auth->make_api_request('POST', '/crm/v2/Contacts', 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
if (isset($contact_id)) {
update_user_meta($user->ID, '_zoho_contact_id', $contact_id);
}
} catch (Exception $e) {
$results['failed']++;
$results['errors'][] = sprintf('User %s: %s', $user->ID, $e->getMessage());
}
}
return $results;
}
/**
* Sync ticket purchases to Zoho Invoices
*
* @return array Sync results
*/
public function sync_purchases() {
$results = array(
'total' => 0,
'synced' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging
);
// Get all completed orders
$orders = wc_get_orders(array(
'status' => 'completed',
'limit' => -1,
'meta_key' => '_tribe_tickets_event_id',
'meta_compare' => 'EXISTS'
));
$results['total'] = 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_invoice_data($order);
$results['test_data'][] = array(
'order_id' => $order->get_id(),
'order_number' => $order->get_order_number(),
'order_total' => $order->get_total(),
'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_invoice_data($order);
// Check if invoice already exists in Zoho
$order_number = $order->get_order_number();
$search_response = $this->auth->make_api_request('GET', '/crm/v2/Invoices/search', array(
'criteria' => "(Invoice_Number:equals:{$order_number})"
));
if (!empty($search_response['data'])) {
// Update existing invoice
$invoice_id = $search_response['data'][0]['id'];
$update_response = $this->auth->make_api_request('PUT', "/crm/v2/Invoices/{$invoice_id}", array(
'data' => array($invoice_data)
));
} else {
// Create new invoice
$create_response = $this->auth->make_api_request('POST', '/crm/v2/Invoices', array(
'data' => array($invoice_data)
));
}
$results['synced']++;
// Update order meta with Zoho ID
if (isset($invoice_id)) {
$order->update_meta_data('_zoho_invoice_id', $invoice_id);
$order->save();
}
} catch (Exception $e) {
$results['failed']++;
$results['errors'][] = sprintf('Order %s: %s', $order->get_id(), $e->getMessage());
}
}
return $results;
}
/**
* 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 = tribe_get_venue($event->ID);
return array(
'Campaign_Name' => get_the_title($event->ID),
'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' => get_the_content(null, false, $event),
'Type' => 'Training Event',
'Expected_Revenue' => floatval(get_post_meta($event->ID, '_price', true)),
'Total_Capacity' => intval(get_post_meta($event->ID, '_stock', true)),
'Venue' => $venue ? get_the_title($venue) : '',
'Trainer_Name' => $trainer ? $trainer->display_name : '',
'Trainer_Email' => $trainer ? $trainer->user_email : '',
'WordPress_Event_ID' => $event->ID
);
}
/**
* Prepare contact data for Zoho
*
* @param WP_User $user User object
* @return array Contact data
*/
private function prepare_contact_data($user) {
$role = in_array('trainer', $user->roles) ? 'Trainer' : 'Trainee';
return array(
'First_Name' => get_user_meta($user->ID, 'first_name', true),
'Last_Name' => get_user_meta($user->ID, 'last_name', true),
'Email' => $user->user_email,
'Phone' => get_user_meta($user->ID, 'phone_number', true),
'Title' => get_user_meta($user->ID, 'hvac_professional_title', true),
'Company' => get_user_meta($user->ID, 'hvac_company_name', true),
'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 Zoho
*
* @param WC_Order $order Order object
* @return array Invoice data
*/
private function prepare_invoice_data($order) {
$event_id = $order->get_meta('_tribe_tickets_event_id');
$event_title = get_the_title($event_id);
$customer = $order->get_user();
// Get contact ID from Zoho
$contact_id = null;
if ($customer) {
$contact_id = get_user_meta($customer->ID, '_zoho_contact_id', true);
}
$items = array();
foreach ($order->get_items() as $item) {
$items[] = array(
'Product_Name' => $item->get_name(),
'Quantity' => $item->get_quantity(),
'Rate' => $item->get_subtotal() / $item->get_quantity(),
'Total' => $item->get_total()
);
}
return array(
'Invoice_Number' => $order->get_order_number(),
'Invoice_Date' => $order->get_date_created()->format('Y-m-d'),
'Status' => 'Paid',
'Contact_Name' => $contact_id,
'Subject' => "Ticket Purchase - {$event_title}",
'Sub_Total' => $order->get_subtotal(),
'Tax' => $order->get_total_tax(),
'Total' => $order->get_total(),
'Balance' => 0,
'WordPress_Order_ID' => $order->get_id(),
'Product_Details' => $items
);
}
}
?>

View file

@ -0,0 +1,155 @@
<?php
/**
* Zoho CRM Setup Helper
*
* Run this script from command line to help set up Zoho credentials
* Usage: php setup-helper.php
*/
// Check if running from command line
if (php_sapi_name() !== 'cli') {
die('This script must be run from the command line.');
}
echo "\n=== Zoho CRM Setup Helper ===\n\n";
// Step 1: Get Client Credentials
echo "Step 1: Enter your Zoho OAuth Client details\n";
echo "----------------------------------------\n";
echo "Client ID: ";
$client_id = trim(fgets(STDIN));
echo "Client Secret: ";
$client_secret = trim(fgets(STDIN));
echo "Redirect URI (default: http://localhost:8080/callback): ";
$redirect_uri = trim(fgets(STDIN));
if (empty($redirect_uri)) {
$redirect_uri = 'http://localhost:8080/callback';
}
// Step 2: Generate Authorization URL
$scopes = 'ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.users.all,ZohoCRM.org.all';
$auth_url = "https://accounts.zoho.com/oauth/v2/auth?" . http_build_query([
'scope' => $scopes,
'client_id' => $client_id,
'response_type' => 'code',
'access_type' => 'offline',
'redirect_uri' => $redirect_uri,
'prompt' => 'consent'
]);
echo "\nStep 2: Authorize the application\n";
echo "--------------------------------\n";
echo "Open this URL in your browser:\n\n";
echo $auth_url . "\n\n";
echo "After authorization, you'll be redirected to:\n";
echo $redirect_uri . "?code=AUTH_CODE\n\n";
echo "Enter the authorization code: ";
$auth_code = trim(fgets(STDIN));
// Step 3: Exchange code for tokens
echo "\nStep 3: Exchanging code for tokens...\n";
echo "-----------------------------------\n";
$token_url = 'https://accounts.zoho.com/oauth/v2/token';
$token_params = [
'grant_type' => 'authorization_code',
'client_id' => $client_id,
'client_secret' => $client_secret,
'redirect_uri' => $redirect_uri,
'code' => $auth_code
];
$ch = curl_init($token_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($token_params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
echo "Error: Failed to get tokens (HTTP $http_code)\n";
echo "Response: " . $response . "\n";
exit(1);
}
$token_data = json_decode($response, true);
if (!isset($token_data['access_token']) || !isset($token_data['refresh_token'])) {
echo "Error: Invalid token response\n";
echo "Response: " . $response . "\n";
exit(1);
}
echo "Success! Tokens received.\n\n";
// Step 4: Get Organization ID
echo "Step 4: Getting organization ID...\n";
echo "--------------------------------\n";
$org_url = 'https://www.zohoapis.com/crm/v2/org';
$ch = curl_init($org_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Zoho-oauthtoken ' . $token_data['access_token']
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$org_response = curl_exec($ch);
$org_data = json_decode($org_response, true);
curl_close($ch);
$org_id = isset($org_data['org'][0]['id']) ? $org_data['org'][0]['id'] : 'NOT_FOUND';
// Step 5: Generate config file
echo "\nStep 5: Generating configuration\n";
echo "-------------------------------\n";
$config_content = "<?php
/**
* Zoho CRM Configuration
* Generated on: " . date('Y-m-d H:i:s') . "
*
* DO NOT commit this file to version control!
*/
// Zoho OAuth Credentials
define('ZOHO_CLIENT_ID', '$client_id');
define('ZOHO_CLIENT_SECRET', '$client_secret');
define('ZOHO_REFRESH_TOKEN', '{$token_data['refresh_token']}');
define('ZOHO_REDIRECT_URI', '$redirect_uri');
// Zoho API Settings
define('ZOHO_API_BASE_URL', 'https://www.zohoapis.com');
define('ZOHO_ACCOUNTS_URL', 'https://accounts.zoho.com');
define('ZOHO_ORGANIZATION_ID', '$org_id');
// API Scopes
define('ZOHO_SCOPES', '$scopes');
// Development/Production flag
define('ZOHO_ENVIRONMENT', 'development');
// Error logging
define('ZOHO_DEBUG_MODE', true);
define('ZOHO_LOG_FILE', WP_CONTENT_DIR . '/zoho-crm-debug.log');
";
// Save config file
$config_file = __DIR__ . '/zoho-config.php';
file_put_contents($config_file, $config_content);
echo "Configuration saved to: $config_file\n\n";
echo "=== Setup Complete! ===\n";
echo "Your Zoho CRM integration is ready to use.\n";
echo "Refresh token: {$token_data['refresh_token']}\n";
echo "Organization ID: $org_id\n\n";
echo "Next steps:\n";
echo "1. The config file has been created at: $config_file\n";
echo "2. Make sure to keep this file secure and never commit it to version control\n";
echo "3. You can now use the Zoho CRM integration in your WordPress plugin\n\n";

View file

@ -0,0 +1,217 @@
<?php
/**
* Test Zoho CRM Integration
*
* Run this script to test the Zoho integration and complete the setup
* Usage: php test-integration.php
*/
// Load environment variables
$env_file = '/Users/ben/dev/upskill-event-manager/wordpress-dev/.env';
if (file_exists($env_file)) {
$lines = file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos($line, '=') !== false && strpos($line, '#') !== 0) {
list($key, $value) = explode('=', $line, 2);
putenv(trim($key) . '=' . trim($value));
}
}
} else {
die("Error: .env file not found at $env_file\n");
}
// Check if running from command line
if (php_sapi_name() !== 'cli') {
die('This script must be run from the command line.');
}
echo "\n=== Zoho CRM Integration Test ===\n\n";
// Get credentials from environment
$client_id = getenv('ZOHO_CLIENT_ID');
$client_secret = getenv('ZOHO_CLIENT_SECRET');
if (!$client_id || !$client_secret) {
die("Error: ZOHO_CLIENT_ID and ZOHO_CLIENT_SECRET not found in environment variables.\n");
}
echo "✓ Credentials loaded from .env file\n";
echo "Client ID: " . substr($client_id, 0, 20) . "...\n\n";
// Set redirect URI
$redirect_uri = 'http://localhost:8080/callback';
// Step 1: Generate Authorization URL
$scopes = 'ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.users.all,ZohoCRM.org.all';
$auth_url = "https://accounts.zoho.com/oauth/v2/auth?" . http_build_query([
'scope' => $scopes,
'client_id' => $client_id,
'response_type' => 'code',
'access_type' => 'offline',
'redirect_uri' => $redirect_uri,
'prompt' => 'consent'
]);
echo "Step 1: Authorization\n";
echo "--------------------\n";
echo "Please open this URL in your browser:\n\n";
echo $auth_url . "\n\n";
echo "After authorization, you'll be redirected to:\n";
echo $redirect_uri . "?code=AUTH_CODE\n\n";
echo "Enter the authorization code from the URL: ";
$auth_code = trim(fgets(STDIN));
// Step 2: Exchange code for tokens
echo "\nStep 2: Exchanging code for tokens...\n";
echo "-----------------------------------\n";
$token_url = 'https://accounts.zoho.com/oauth/v2/token';
$token_params = [
'grant_type' => 'authorization_code',
'client_id' => $client_id,
'client_secret' => $client_secret,
'redirect_uri' => $redirect_uri,
'code' => $auth_code
];
$ch = curl_init($token_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($token_params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
echo "Error: Failed to get tokens (HTTP $http_code)\n";
echo "Response: " . $response . "\n";
exit(1);
}
$token_data = json_decode($response, true);
if (!isset($token_data['access_token']) || !isset($token_data['refresh_token'])) {
echo "Error: Invalid token response\n";
echo "Response: " . $response . "\n";
exit(1);
}
echo "✓ Tokens received successfully\n";
echo "Access Token: " . substr($token_data['access_token'], 0, 20) . "...\n";
echo "Refresh Token: " . substr($token_data['refresh_token'], 0, 20) . "...\n\n";
// Step 3: Get Organization Info
echo "Step 3: Getting organization information...\n";
echo "-----------------------------------------\n";
$org_url = 'https://www.zohoapis.com/crm/v2/org';
$ch = curl_init($org_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Zoho-oauthtoken ' . $token_data['access_token']
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$org_response = curl_exec($ch);
$org_data = json_decode($org_response, true);
curl_close($ch);
if (isset($org_data['org'][0])) {
$org = $org_data['org'][0];
echo "✓ Organization found\n";
echo "Name: " . $org['company_name'] . "\n";
echo "ID: " . $org['id'] . "\n";
echo "Time Zone: " . $org['time_zone'] . "\n\n";
} else {
echo "Error: Could not get organization info\n";
echo "Response: " . $org_response . "\n";
}
// Step 4: Test Module Access
echo "Step 4: Testing module access...\n";
echo "-------------------------------\n";
$modules = ['Campaigns', 'Contacts', 'Invoices'];
foreach ($modules as $module) {
$module_url = "https://www.zohoapis.com/crm/v2/settings/modules/$module";
$ch = curl_init($module_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Zoho-oauthtoken ' . $token_data['access_token']
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$module_response = curl_exec($ch);
$module_data = json_decode($module_response, true);
curl_close($ch);
if (isset($module_data['modules'][0])) {
echo "$module module accessible\n";
} else {
echo "$module module not accessible\n";
}
}
// Step 5: Create configuration file
echo "\nStep 5: Creating configuration file...\n";
echo "-------------------------------------\n";
$config_content = "<?php
/**
* Zoho CRM Configuration
* Generated on: " . date('Y-m-d H:i:s') . "
*
* DO NOT commit this file to version control!
*/
// Zoho OAuth Credentials
define('ZOHO_CLIENT_ID', '" . $client_id . "');
define('ZOHO_CLIENT_SECRET', '" . $client_secret . "');
define('ZOHO_REFRESH_TOKEN', '" . $token_data['refresh_token'] . "');
define('ZOHO_REDIRECT_URI', '" . $redirect_uri . "');
// Zoho API Settings
define('ZOHO_API_BASE_URL', 'https://www.zohoapis.com');
define('ZOHO_ACCOUNTS_URL', 'https://accounts.zoho.com');
define('ZOHO_ORGANIZATION_ID', '" . (isset($org['id']) ? $org['id'] : 'NOT_FOUND') . "');
// API Scopes
define('ZOHO_SCOPES', '" . $scopes . "');
// Development/Production flag
define('ZOHO_ENVIRONMENT', 'development');
// Error logging
define('ZOHO_DEBUG_MODE', true);
define('ZOHO_LOG_FILE', WP_CONTENT_DIR . '/zoho-crm-debug.log');
";
$config_file = __DIR__ . '/zoho-config.php';
file_put_contents($config_file, $config_content);
echo "✓ Configuration file created: $config_file\n\n";
// Step 6: Update .env file with refresh token
echo "Step 6: Updating .env file...\n";
echo "----------------------------\n";
$env_content = file_get_contents($env_file);
if (strpos($env_content, 'ZOHO_REFRESH_TOKEN') === false) {
// Add refresh token to .env
$env_content .= "\n# Zoho refresh token (auto-generated)\n";
$env_content .= "ZOHO_REFRESH_TOKEN=" . $token_data['refresh_token'] . "\n";
$env_content .= "ZOHO_ORGANIZATION_ID=" . (isset($org['id']) ? $org['id'] : 'NOT_FOUND') . "\n";
file_put_contents($env_file, $env_content);
echo "✓ Added refresh token to .env file\n";
} else {
echo " Refresh token already exists in .env file\n";
}
echo "\n=== Integration Test Complete! ===\n";
echo "Your Zoho CRM integration is ready to use.\n";
echo "Next steps:\n";
echo "1. The system will automatically create custom fields in Zoho\n";
echo "2. You can start syncing events, contacts, and invoices\n";
echo "3. Check the WordPress admin for integration status\n\n";

View file

@ -0,0 +1,33 @@
<?php
/**
* Zoho CRM Configuration Template
*
* Copy this file to zoho-config.php and fill in your credentials
* DO NOT commit zoho-config.php to version control!
*/
// Zoho OAuth Credentials - Load from environment if available
define('ZOHO_CLIENT_ID', getenv('ZOHO_CLIENT_ID') ?: 'YOUR_CLIENT_ID_HERE');
define('ZOHO_CLIENT_SECRET', getenv('ZOHO_CLIENT_SECRET') ?: 'YOUR_CLIENT_SECRET_HERE');
define('ZOHO_REFRESH_TOKEN', getenv('ZOHO_REFRESH_TOKEN') ?: 'YOUR_REFRESH_TOKEN_HERE');
define('ZOHO_REDIRECT_URI', getenv('ZOHO_REDIRECT_URI') ?: 'http://localhost:8080/callback');
// Zoho API Settings
define('ZOHO_API_BASE_URL', 'https://www.zohoapis.com');
define('ZOHO_ACCOUNTS_URL', 'https://accounts.zoho.com');
define('ZOHO_ORGANIZATION_ID', 'YOUR_ORG_ID_HERE');
// API Scopes
define('ZOHO_SCOPES', 'ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.users.all');
// Optional: Region-specific settings
// For EU: 'https://accounts.zoho.eu' and 'https://www.zohoapis.eu'
// For IN: 'https://accounts.zoho.in' and 'https://www.zohoapis.in'
// For AU: 'https://accounts.zoho.com.au' and 'https://www.zohoapis.com.au'
// Development/Production flag
define('ZOHO_ENVIRONMENT', 'development'); // 'development' or 'production'
// Error logging
define('ZOHO_DEBUG_MODE', true);
define('ZOHO_LOG_FILE', WP_CONTENT_DIR . '/zoho-crm-debug.log');

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="tests/bootstrap.php"
executionOrder="depends,defects"
forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="false"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
verbose="true"
>
<testsuites>
<testsuite name="unit">
<directory suffix="test.php">tests/unit</directory>
</testsuite>
<testsuite name="integration">
<directory suffix="test.php">tests/integration</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="false">
<include>
<directory suffix=".php">includes</directory>
</include>
<exclude>
<directory>vendor</directory>
<directory>tests</directory>
</exclude>
</coverage>
<php>
<const name="HVAC_TESTING" value="true"/>
<const name="WP_DEBUG" value="true"/>
</php>
</phpunit>

View file

@ -0,0 +1,86 @@
# HVAC Community Events Plugin Refactoring Plan
## Overview
The plugin has decent structure but needs improvements in several areas:
## 1. Debug Logging Cleanup
- **Issue**: Excessive debug logging throughout codebase
- **Solution**:
- Create a centralized logging class
- Use conditional logging based on WP_DEBUG or custom debug flag
- Remove or consolidate redundant log statements
## 2. Class Organization
- **Issue**: Inconsistent namespace usage and class dependencies
- **Solution**:
- Implement proper PSR-4 autoloading
- Use consistent namespacing throughout
- Create proper singleton patterns for main classes
## 3. Security Enhancements
- **Issue**: Input validation could be improved in several areas
- **Solution**:
- Add more robust nonce verification
- Implement stricter capability checks
- Add proper escaping for all outputs
## 4. Database Query Optimization
- **Issue**: Multiple queries for dashboard stats, inconsistent approach to querying events
- **Solution**:
- Consolidate queries where possible
- Use proper WordPress caching mechanisms
- Fix the inconsistency between post_author and _EventOrganizerID
## 5. Registration Form Improvements
- **Issue**: Very large single class (1066 lines) handling too many responsibilities
- **Solution**:
- Split into separate classes for validation, user creation, notifications
- Create a form builder pattern
- Implement better error handling with proper exceptions
## 6. Template System
- **Issue**: Direct include of templates without proper hooks
- **Solution**:
- Implement proper template hierarchy
- Add filters for template overrides
- Use locate_template pattern
## 7. Asset Management
- **Issue**: Basic enqueuing without version control or dependencies
- **Solution**:
- Implement asset versioning based on file modification times
- Add proper dependency management
- Minify production assets
## 8. Code Standards
- **Issue**: Mixed coding standards and formatting
- **Solution**:
- Implement WordPress Coding Standards
- Use PHP_CodeSniffer with WordPress rules
- Add proper PHPDoc comments
## 9. Testing Infrastructure
- **Issue**: No proper unit tests
- **Solution**:
- Add PHPUnit test infrastructure
- Create test doubles for WordPress functions
- Implement integration tests for critical paths
## 10. Configuration Management
- **Issue**: Hard-coded values and paths
- **Solution**:
- Create configuration class
- Use WordPress options API properly
- Implement settings page for admin configuration
## Implementation Priority
1. Debug logging cleanup (Quick win)
2. Security enhancements
3. Registration form refactoring
4. Database optimization
5. Code standards implementation
6. Template system improvements
7. Testing infrastructure
8. Asset management
9. Configuration management
10. Full namespace implementation

View file

@ -0,0 +1,230 @@
<?php
/**
* Integration test for dashboard flow
*
* @package HVAC_Community_Events
* @subpackage Tests
*/
/**
* Class Test_Dashboard_Flow
*/
class Test_Dashboard_Flow extends WP_UnitTestCase {
/**
* Test user
*
* @var WP_User
*/
private $trainer_user;
/**
* Setup test
*/
public function setUp(): void {
parent::setUp();
// Create HVAC trainer role if it doesn't exist
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php';
$roles = new HVAC_Roles();
$roles->create_trainer_role();
// Create test trainer user
$this->trainer_user = new WP_User( $this->factory->user->create( array(
'user_login' => 'test_trainer',
'user_email' => 'trainer@example.com',
'role' => 'hvac_trainer',
) ) );
// Set annual revenue target
update_user_meta( $this->trainer_user->ID, 'annual_revenue_target', 50000 );
}
/**
* Teardown test
*/
public function tearDown(): void {
// Clean up user
wp_delete_user( $this->trainer_user->ID );
parent::tearDown();
}
/**
* Test dashboard access control
*/
public function test_dashboard_access_control() {
// Non-logged-in user should not have access
$this->assertFalse( current_user_can( 'view_hvac_dashboard' ) );
// Regular subscriber should not have access
$subscriber = new WP_User( $this->factory->user->create( array(
'role' => 'subscriber',
) ) );
wp_set_current_user( $subscriber->ID );
$this->assertFalse( current_user_can( 'view_hvac_dashboard' ) );
// Trainer should have access
wp_set_current_user( $this->trainer_user->ID );
$this->assertTrue( current_user_can( 'view_hvac_dashboard' ) );
// Clean up
wp_delete_user( $subscriber->ID );
wp_set_current_user( 0 );
}
/**
* Test dashboard data loading
*/
public function test_dashboard_data_loading() {
wp_set_current_user( $this->trainer_user->ID );
// Load dashboard data
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-dashboard-data-refactored.php';
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->trainer_user->ID );
// Get initial stats (should be empty)
$stats = $dashboard_data->get_all_stats();
$this->assertEquals( 0, $stats['total_events'] );
$this->assertEquals( 0, $stats['upcoming_events'] );
$this->assertEquals( 0, $stats['past_events'] );
$this->assertEquals( 0, $stats['total_tickets'] );
$this->assertEquals( 0, $stats['total_revenue'] );
$this->assertEquals( 50000, $stats['revenue_target'] );
}
/**
* Test dashboard with events
*/
public function test_dashboard_with_events() {
wp_set_current_user( $this->trainer_user->ID );
// Create test events
$events = array();
// Past event
$events[] = $this->factory->post->create( array(
'post_type' => 'tribe_events',
'post_status' => 'publish',
'post_author' => $this->trainer_user->ID,
'post_title' => 'Past Training Event',
) );
update_post_meta( $events[0], '_EventStartDate', date( 'Y-m-d H:i:s', strtotime( '-1 week' ) ) );
update_post_meta( $events[0], '_EventEndDate', date( 'Y-m-d H:i:s', strtotime( '-1 week +3 hours' ) ) );
update_post_meta( $events[0], '_tribe_tickets_sold', 15 );
update_post_meta( $events[0], '_tribe_revenue_total', 1500 );
// Future event
$events[] = $this->factory->post->create( array(
'post_type' => 'tribe_events',
'post_status' => 'publish',
'post_author' => $this->trainer_user->ID,
'post_title' => 'Upcoming Training Event',
) );
update_post_meta( $events[1], '_EventStartDate', date( 'Y-m-d H:i:s', strtotime( '+1 week' ) ) );
update_post_meta( $events[1], '_EventEndDate', date( 'Y-m-d H:i:s', strtotime( '+1 week +3 hours' ) ) );
update_post_meta( $events[1], '_tribe_tickets_sold', 5 );
update_post_meta( $events[1], '_tribe_revenue_total', 500 );
// Draft event
$events[] = $this->factory->post->create( array(
'post_type' => 'tribe_events',
'post_status' => 'draft',
'post_author' => $this->trainer_user->ID,
'post_title' => 'Draft Training Event',
) );
update_post_meta( $events[2], '_EventStartDate', date( 'Y-m-d H:i:s', strtotime( '+2 weeks' ) ) );
update_post_meta( $events[2], '_tribe_tickets_sold', 0 );
update_post_meta( $events[2], '_tribe_revenue_total', 0 );
// Load dashboard data
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-dashboard-data-refactored.php';
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->trainer_user->ID );
// Get stats
$stats = $dashboard_data->get_all_stats();
$this->assertEquals( 3, $stats['total_events'] );
$this->assertEquals( 1, $stats['upcoming_events'] );
$this->assertEquals( 1, $stats['past_events'] );
$this->assertEquals( 20, $stats['total_tickets'] );
$this->assertEquals( 2000, $stats['total_revenue'] );
// Get events table data
$table_data = $dashboard_data->get_events_table_data();
$this->assertCount( 3, $table_data );
// Clean up
foreach ( $events as $event_id ) {
wp_delete_post( $event_id, true );
}
}
/**
* Test dashboard template rendering
*/
public function test_dashboard_template() {
wp_set_current_user( $this->trainer_user->ID );
// Capture output
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php';
$output = ob_get_clean();
// Verify template elements
$this->assertStringContainsString( 'Trainer Dashboard', $output );
$this->assertStringContainsString( 'Your Stats', $output );
$this->assertStringContainsString( 'Total Events', $output );
$this->assertStringContainsString( 'Upcoming Events', $output );
$this->assertStringContainsString( 'Total Revenue', $output );
$this->assertStringContainsString( 'Your Events', $output );
}
/**
* Test admin area redirection
*/
public function test_admin_redirection() {
// Trainers should be redirected from admin area
wp_set_current_user( $this->trainer_user->ID );
// The HVAC_Community_Events class should handle this
$community_events = HVAC_Community_Events::instance();
// Can't test actual redirect in unit tests, but can verify capability
$this->assertTrue( current_user_can( 'view_hvac_dashboard' ) );
$this->assertFalse( current_user_can( 'manage_options' ) );
}
/**
* Test revenue target display
*/
public function test_revenue_target_display() {
wp_set_current_user( $this->trainer_user->ID );
// Create an event with revenue
$event_id = $this->factory->post->create( array(
'post_type' => 'tribe_events',
'post_status' => 'publish',
'post_author' => $this->trainer_user->ID,
'post_title' => 'Revenue Event',
) );
update_post_meta( $event_id, '_tribe_revenue_total', 5000 );
// Load dashboard data
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-dashboard-data-refactored.php';
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->trainer_user->ID );
$stats = $dashboard_data->get_all_stats();
// Verify revenue and target
$this->assertEquals( 5000, $stats['total_revenue'] );
$this->assertEquals( 50000, $stats['revenue_target'] );
// Revenue is 10% of target
$percentage = ( $stats['total_revenue'] / $stats['revenue_target'] ) * 100;
$this->assertEquals( 10, $percentage );
// Clean up
wp_delete_post( $event_id, true );
}
}

View file

@ -0,0 +1,298 @@
<?php
/**
* Event Submission Flow Integration Test
*
* @package HVAC_Community_Events
* @subpackage Tests\Integration
*/
/**
* Test the complete event submission flow
*/
class Test_Event_Submission_Flow extends WP_UnitTestCase {
/**
* Test user
*
* @var int
*/
private $test_user_id;
/**
* Set up before each test
*/
public function setUp(): void {
parent::setUp();
// Create test user with trainer role
$this->test_user_id = $this->factory->user->create( array(
'role' => 'contributor',
'display_name' => 'Test Trainer',
) );
// Set current user
wp_set_current_user( $this->test_user_id );
// Enable debug logging
add_filter( 'hvac_ce_debug_mode', '__return_true' );
HVAC_Logger::init();
}
/**
* Test complete event submission flow
*/
public function test_event_submission_flow() {
// Step 1: Initialize form
$form_builder = new HVAC_Form_Builder();
$form_builder->init( array(
'id' => 'event-submission-form',
'nonce_action' => 'submit_event',
) );
// Step 2: Add form fields
$form_builder->add_field( 'text', array(
'name' => 'event_title',
'label' => 'Event Title',
'required' => true,
) );
$form_builder->add_field( 'textarea', array(
'name' => 'event_description',
'label' => 'Event Description',
'required' => true,
) );
$form_builder->add_field( 'date', array(
'name' => 'event_start_date',
'label' => 'Start Date',
'required' => true,
) );
$form_builder->add_field( 'time', array(
'name' => 'event_start_time',
'label' => 'Start Time',
'required' => true,
) );
// Step 3: Simulate form data
$form_data = array(
'event_title' => 'Test HVAC Training Event',
'event_description' => 'This is a test event description',
'event_start_date' => date( 'Y-m-d', strtotime( '+1 week' ) ),
'event_start_time' => '14:00',
'_wpnonce' => wp_create_nonce( 'submit_event' ),
);
$form_builder->set_data( $form_data );
// Step 4: Validate form
$is_valid = $form_builder->validate();
$this->assertTrue( $is_valid );
// Step 5: Process submission
if ( $is_valid ) {
// Simulate event creation
$event_data = array(
'post_title' => $form_data['event_title'],
'post_content' => $form_data['event_description'],
'post_type' => 'tribe_events',
'post_status' => 'draft', // Community events start as draft
'post_author' => $this->test_user_id,
);
$event_id = wp_insert_post( $event_data );
$this->assertNotWPError( $event_id );
$this->assertGreaterThan( 0, $event_id );
// Add event meta
$start_datetime = $form_data['event_start_date'] . ' ' . $form_data['event_start_time'] . ':00';
update_post_meta( $event_id, '_EventStartDate', $start_datetime );
update_post_meta( $event_id, '_EventEndDate', date( 'Y-m-d H:i:s', strtotime( $start_datetime . ' +2 hours' ) ) );
// Log the submission
HVAC_Logger::info( 'Event submitted', 'Event Submission', array(
'event_id' => $event_id,
'user_id' => $this->test_user_id,
) );
}
// Step 6: Verify event was created
$event = get_post( $event_id );
$this->assertNotNull( $event );
$this->assertEquals( 'Test HVAC Training Event', $event->post_title );
$this->assertEquals( 'draft', $event->post_status );
$this->assertEquals( $this->test_user_id, $event->post_author );
// Step 7: Check logs
$logs = HVAC_Logger::get_logs();
$this->assertNotEmpty( $logs );
$submission_log = array_filter( $logs, function( $log ) {
return $log['message'] === 'Event submitted';
} );
$this->assertCount( 1, $submission_log );
}
/**
* Test event submission with validation errors
*/
public function test_event_submission_with_errors() {
$form_builder = new HVAC_Form_Builder();
$form_builder->init( array(
'id' => 'event-submission-form',
'nonce_action' => 'submit_event',
) );
$form_builder->add_field( 'text', array(
'name' => 'event_title',
'label' => 'Event Title',
'required' => true,
'validation' => array(
'min_length' => 5,
),
) );
// Submit with invalid data
$form_data = array(
'event_title' => 'Test', // Too short
'_wpnonce' => wp_create_nonce( 'submit_event' ),
);
$form_builder->set_data( $form_data );
$is_valid = $form_builder->validate();
$this->assertFalse( $is_valid );
$errors = $form_builder->get_errors();
$this->assertArrayHasKey( 'event_title', $errors );
// Log validation error
HVAC_Logger::warning( 'Event submission validation failed', 'Event Submission', array(
'errors' => $errors,
'user_id' => $this->test_user_id,
) );
// Check logs
$logs = HVAC_Logger::get_logs( 'warning' );
$this->assertNotEmpty( $logs );
}
/**
* Test unauthorized event submission
*/
public function test_unauthorized_submission() {
// Log out current user
wp_set_current_user( 0 );
// Try to submit without being logged in
$can_submit = current_user_can( 'publish_posts' );
$this->assertFalse( $can_submit );
// Log security warning
HVAC_Logger::warning( 'Unauthorized event submission attempt', 'Security', array(
'ip' => '127.0.0.1',
) );
// Check security logs
$logs = HVAC_Logger::get_logs( 'warning' );
$security_logs = array_filter( $logs, function( $log ) {
return $log['context'] === 'Security';
} );
$this->assertNotEmpty( $security_logs );
}
/**
* Test event update flow
*/
public function test_event_update_flow() {
// Create an event first
$event_id = $this->factory->post->create( array(
'post_title' => 'Original Event Title',
'post_type' => 'tribe_events',
'post_status' => 'draft',
'post_author' => $this->test_user_id,
) );
// Verify user can edit their own event
$can_edit = current_user_can( 'edit_post', $event_id );
$this->assertTrue( $can_edit );
// Update event
$updated_data = array(
'ID' => $event_id,
'post_title' => 'Updated Event Title',
'post_content' => 'Updated event description',
);
$result = wp_update_post( $updated_data );
$this->assertEquals( $event_id, $result );
// Verify update
$updated_event = get_post( $event_id );
$this->assertEquals( 'Updated Event Title', $updated_event->post_title );
$this->assertEquals( 'Updated event description', $updated_event->post_content );
// Log the update
HVAC_Logger::info( 'Event updated', 'Event Update', array(
'event_id' => $event_id,
'user_id' => $this->test_user_id,
) );
}
/**
* Test event deletion flow
*/
public function test_event_deletion_flow() {
// Create an event
$event_id = $this->factory->post->create( array(
'post_title' => 'Event to Delete',
'post_type' => 'tribe_events',
'post_status' => 'draft',
'post_author' => $this->test_user_id,
) );
// Verify user can delete their own event
$can_delete = current_user_can( 'delete_post', $event_id );
$this->assertTrue( $can_delete );
// Delete event (move to trash)
$result = wp_trash_post( $event_id );
$this->assertEquals( $event_id, $result );
// Verify deletion
$deleted_event = get_post( $event_id );
$this->assertEquals( 'trash', $deleted_event->post_status );
// Log the deletion
HVAC_Logger::info( 'Event deleted', 'Event Deletion', array(
'event_id' => $event_id,
'user_id' => $this->test_user_id,
) );
}
/**
* Test rate limiting for event submissions
*/
public function test_submission_rate_limiting() {
$user_id = $this->test_user_id;
$action = 'submit_event';
// Test rate limiting (3 submissions per hour)
for ( $i = 1; $i <= 4; $i++ ) {
$can_submit = HVAC_Security::check_rate_limit( $user_id, $action, 3, 3600 );
if ( $i <= 3 ) {
$this->assertTrue( $can_submit, "Submission $i should be allowed" );
} else {
$this->assertFalse( $can_submit, "Submission $i should be blocked" );
}
}
// Check logs for rate limit warning
$logs = HVAC_Logger::get_logs( 'warning' );
$rate_limit_logs = array_filter( $logs, function( $log ) {
return strpos( $log['message'], 'Rate limit exceeded' ) !== false;
} );
$this->assertNotEmpty( $rate_limit_logs );
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* Setup test events with tickets and attendees
* Run this script via WP-CLI: wp eval-file tests/setup-test-events.php
*/
// Exit if not running in WP-CLI
if (!defined('WP_CLI')) {
echo "This script must be run via WP-CLI\n";
exit(1);
}
// Get test trainer user
$trainer_user = get_user_by('login', 'test_trainer');
if (!$trainer_user) {
echo "Error: test_trainer user not found\n";
exit(1);
}
$trainer_id = $trainer_user->ID;
echo "Found test_trainer user ID: $trainer_id\n";
// Event data
$events = [
[
'title' => 'HVAC System Maintenance Workshop',
'description' => 'Learn essential maintenance techniques for residential and commercial HVAC systems.',
'price' => 200,
'attendees' => 5,
'start_date' => '2025-02-01 09:00:00',
'end_date' => '2025-02-01 17:00:00',
],
[
'title' => 'Advanced HVAC Diagnostics Training',
'description' => 'Master diagnostic tools and techniques for troubleshooting complex HVAC issues.',
'price' => 500,
'attendees' => 12,
'start_date' => '2025-02-15 08:30:00',
'end_date' => '2025-02-15 18:30:00',
],
[
'title' => 'HVAC Installation Best Practices',
'description' => 'Professional installation methods and safety procedures for HVAC technicians.',
'price' => 100,
'attendees' => 2,
'start_date' => '2025-03-01 10:00:00',
'end_date' => '2025-03-01 16:00:00',
],
[
'title' => 'Commercial HVAC Systems Overview',
'description' => 'Understanding large-scale commercial HVAC systems and their components.',
'price' => 750,
'attendees' => 8,
'start_date' => '2025-03-15 09:00:00',
'end_date' => '2025-03-15 18:00:00',
],
[
'title' => 'HVAC Energy Efficiency Certification',
'description' => 'Green HVAC technologies and energy-saving strategies for modern systems.',
'price' => 1000,
'attendees' => 20,
'start_date' => '2025-04-01 08:00:00',
'end_date' => '2025-04-01 17:00:00',
],
];
// First names and last names for random generation
$first_names = ['John', 'Jane', 'Michael', 'Sarah', 'Robert', 'Emily', 'David', 'Jessica', 'James', 'Jennifer',
'William', 'Linda', 'Richard', 'Barbara', 'Joseph', 'Susan', 'Thomas', 'Karen', 'Charles', 'Nancy'];
$last_names = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez',
'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin'];
// Create events
foreach ($events as $index => $event_data) {
echo "\nCreating event: {$event_data['title']}\n";
// Create the event post
$event_args = [
'post_title' => $event_data['title'],
'post_content' => $event_data['description'],
'post_status' => 'publish',
'post_type' => 'tribe_events',
'post_author' => $trainer_id,
'meta_input' => [
'_EventStartDate' => $event_data['start_date'],
'_EventEndDate' => $event_data['end_date'],
'_EventStartDateUTC' => $event_data['start_date'],
'_EventEndDateUTC' => $event_data['end_date'],
'_EventCost' => $event_data['price'],
'_EventCurrencySymbol' => '$',
'_EventTimezone' => 'America/New_York',
'_EventShowMap' => 1,
'_EventShowMapLink' => 1,
],
];
$event_id = wp_insert_post($event_args);
if (is_wp_error($event_id)) {
echo "Error creating event: " . $event_id->get_error_message() . "\n";
continue;
}
echo "Created event ID: $event_id\n";
// Create venue
$venue_args = [
'post_title' => 'HVAC Training Center ' . ($index + 1),
'post_status' => 'publish',
'post_type' => 'tribe_venue',
'post_author' => $trainer_id,
'meta_input' => [
'_VenueAddress' => ($index + 1) . '23 Training Boulevard',
'_VenueCity' => 'Training City',
'_VenueState' => 'NY',
'_VenueZip' => '12345',
'_VenueCountry' => 'United States',
'_VenuePhone' => '555-' . str_pad($index + 1, 4, '0', STR_PAD_LEFT),
],
];
$venue_id = wp_insert_post($venue_args);
update_post_meta($event_id, '_EventVenueID', $venue_id);
// Create organizer
$organizer_args = [
'post_title' => 'HVAC Training Organization',
'post_status' => 'publish',
'post_type' => 'tribe_organizer',
'post_author' => $trainer_id,
'meta_input' => [
'_OrganizerEmail' => 'trainer@hvactraining.com',
'_OrganizerPhone' => '555-0000',
'_OrganizerWebsite' => 'https://hvactraining.com',
],
];
$organizer_id = wp_insert_post($organizer_args);
update_post_meta($event_id, '_EventOrganizerID', $organizer_id);
// Create tickets using Event Tickets
if (class_exists('Tribe__Tickets__Main')) {
$ticket_args = [
'post_title' => 'General Admission',
'post_content' => 'Standard ticket for ' . $event_data['title'],
'post_status' => 'publish',
'post_type' => 'tribe_tpp_tickets',
'post_author' => $trainer_id,
'post_parent' => $event_id,
'meta_input' => [
'_price' => $event_data['price'],
'_stock' => 50,
'_capacity' => 50,
'_ticket_start_date' => date('Y-m-d H:i:s'),
'_ticket_end_date' => $event_data['start_date'],
'_tribe_tpp_for_event' => $event_id,
],
];
$ticket_id = wp_insert_post($ticket_args);
echo "Created ticket ID: $ticket_id\n";
// Create attendees
for ($i = 1; $i <= $event_data['attendees']; $i++) {
$first_name = $first_names[array_rand($first_names)];
$last_name = $last_names[array_rand($last_names)];
$email = strtolower($first_name . '.' . $last_name . '.event' . $event_id . '@test.com');
// Create attendee post
$attendee_args = [
'post_title' => $first_name . ' ' . $last_name,
'post_status' => 'publish',
'post_type' => 'tribe_tpp_attendees',
'post_author' => $trainer_id,
'meta_input' => [
'_tribe_tpp_event' => $event_id,
'_tribe_tpp_product' => $ticket_id,
'_tribe_tpp_full_name' => $first_name . ' ' . $last_name,
'_tribe_tpp_email' => $email,
'_tribe_tpp_attendee_user_id' => 0,
'_tribe_tpp_order_status' => 'completed',
'_tribe_tpp_security_code' => wp_generate_password(10, false),
'_paid_price' => $event_data['price'],
],
];
$attendee_id = wp_insert_post($attendee_args);
// Update ticket sales count
$current_sales = get_post_meta($ticket_id, '_tribe_ticket_sales_count', true);
update_post_meta($ticket_id, '_tribe_ticket_sales_count', $current_sales + 1);
echo "Created attendee: $first_name $last_name (ID: $attendee_id)\n";
}
} else {
echo "Event Tickets plugin not found - skipping ticket creation\n";
}
echo "Created {$event_data['attendees']} attendees for event: {$event_data['title']}\n";
}
echo "\nTest data setup complete!\n";
echo "Created " . count($events) . " events with tickets and attendees\n";
// Display summary
echo "\nEvent Summary:\n";
foreach ($events as $index => $event_data) {
echo "- {$event_data['title']}: {$event_data['attendees']} attendees @ \${$event_data['price']} each\n";
}

View file

@ -0,0 +1,68 @@
<?php
/**
* Test Zoho Staging Mode
*
* This script tests that staging mode correctly prevents production syncs
*/
// Load WordPress
require_once dirname(__DIR__, 5) . '/wp-load.php';
// Load our classes
require_once dirname(__DIR__) . '/includes/zoho/class-zoho-sync.php';
require_once dirname(__DIR__) . '/includes/zoho/class-zoho-crm-auth.php';
echo "\n=== Zoho Staging Mode Test ===\n\n";
// Check current site URL
$site_url = get_site_url();
$is_staging = strpos($site_url, 'upskillhvac.com') === false;
echo "Site URL: " . $site_url . "\n";
echo "Is Staging: " . ($is_staging ? 'YES' : 'NO') . "\n\n";
// Test sync operations
$sync = new HVAC_Zoho_Sync();
echo "Testing Event Sync...\n";
$event_results = $sync->sync_events();
print_r($event_results);
echo "\n";
echo "Testing User Sync...\n";
$user_results = $sync->sync_users();
print_r($user_results);
echo "\n";
echo "Testing Purchase Sync...\n";
$purchase_results = $sync->sync_purchases();
print_r($purchase_results);
echo "\n";
// Verify staging mode behavior
if ($is_staging) {
echo "✓ Staging mode active - verifying no data was sent to Zoho\n";
$checks = array(
'Events' => $event_results,
'Users' => $user_results,
'Purchases' => $purchase_results
);
foreach ($checks as $type => $result) {
if (isset($result['staging_mode']) && $result['staging_mode']) {
echo "$type sync correctly in staging mode\n";
} else {
echo "✗ ERROR: $type sync not in staging mode!\n";
}
if (isset($result['test_data'])) {
echo " - Test data preview available\n";
}
}
} else {
echo "⚠️ Production mode active - data would be sent to Zoho\n";
}
echo "\n=== Test Complete ===\n";
?>

View file

@ -0,0 +1,340 @@
<?php
/**
* Tests for HVAC_Dashboard_Data_Refactored class
*
* @package HVAC_Community_Events
* @subpackage Tests
*/
/**
* Class HVAC_Dashboard_Data_Test
*/
class HVAC_Dashboard_Data_Test extends WP_UnitTestCase {
/**
* Test user ID
*
* @var int
*/
private $user_id;
/**
* Test events
*
* @var array
*/
private $test_events = array();
/**
* Setup test
*/
public function setUp(): void {
parent::setUp();
// Create test user
$this->user_id = $this->factory->user->create( array(
'role' => 'hvac_trainer',
) );
// Mock The Events Calendar constants
if ( ! defined( 'Tribe__Events__Main' ) ) {
define( 'Tribe__Events__Main', 'MockTribeEvents' );
}
if ( ! defined( 'MockTribeEvents::POSTTYPE' ) ) {
define( 'MockTribeEvents::POSTTYPE', 'tribe_events' );
}
}
/**
* Teardown test
*/
public function tearDown(): void {
// Clean up test events
foreach ( $this->test_events as $event_id ) {
wp_delete_post( $event_id, true );
}
// Clean up test user
wp_delete_user( $this->user_id );
parent::tearDown();
}
/**
* Create test event
*
* @param array $args Event arguments
* @return int Event ID
*/
private function create_test_event( $args = array() ) {
$defaults = array(
'post_type' => MockTribeEvents::POSTTYPE,
'post_status' => 'publish',
'post_author' => $this->user_id,
'post_title' => 'Test Event',
);
$args = wp_parse_args( $args, $defaults );
$event_id = $this->factory->post->create( $args );
// Add default meta data
$meta_defaults = array(
'_EventStartDate' => current_time( 'mysql' ),
'_EventEndDate' => current_time( 'mysql' ),
'_EventOrganizerID' => $this->user_id,
'_tribe_tickets_sold' => 0,
'_tribe_revenue_total' => 0,
);
foreach ( $meta_defaults as $key => $value ) {
if ( ! isset( $args['meta'][ $key ] ) ) {
update_post_meta( $event_id, $key, $value );
}
}
// Apply custom meta
if ( isset( $args['meta'] ) ) {
foreach ( $args['meta'] as $key => $value ) {
update_post_meta( $event_id, $key, $value );
}
}
$this->test_events[] = $event_id;
return $event_id;
}
/**
* Test dashboard data initialization
*/
public function test_initialization() {
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
$this->assertInstanceOf( 'HVAC_Dashboard_Data_Refactored', $dashboard_data );
}
/**
* Test get all stats with caching
*/
public function test_get_all_stats() {
// Create test events
$this->create_test_event( array(
'meta' => array(
'_EventStartDate' => date( 'Y-m-d H:i:s', strtotime( '+1 week' ) ),
'_tribe_tickets_sold' => 10,
'_tribe_revenue_total' => 500,
),
) );
$this->create_test_event( array(
'meta' => array(
'_EventStartDate' => date( 'Y-m-d H:i:s', strtotime( '-1 week' ) ),
'_tribe_tickets_sold' => 5,
'_tribe_revenue_total' => 250,
),
) );
// Set revenue target
update_user_meta( $this->user_id, 'annual_revenue_target', 10000 );
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
$stats = $dashboard_data->get_all_stats();
$this->assertIsArray( $stats );
$this->assertArrayHasKey( 'total_events', $stats );
$this->assertArrayHasKey( 'upcoming_events', $stats );
$this->assertArrayHasKey( 'past_events', $stats );
$this->assertArrayHasKey( 'total_tickets', $stats );
$this->assertArrayHasKey( 'total_revenue', $stats );
$this->assertArrayHasKey( 'revenue_target', $stats );
$this->assertEquals( 2, $stats['total_events'] );
$this->assertEquals( 15, $stats['total_tickets'] );
$this->assertEquals( 750, $stats['total_revenue'] );
$this->assertEquals( 10000, $stats['revenue_target'] );
// Test caching - should return same result without recalculating
$stats2 = $dashboard_data->get_all_stats();
$this->assertEquals( $stats, $stats2 );
}
/**
* Test cache clearing
*/
public function test_cache_clearing() {
$this->create_test_event();
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
$stats1 = $dashboard_data->get_all_stats();
// Clear cache
$dashboard_data->clear_cache();
// Create another event
$this->create_test_event();
// Should recalculate and get different results
$stats2 = $dashboard_data->get_all_stats();
$this->assertNotEquals( $stats1['total_events'], $stats2['total_events'] );
}
/**
* Test upcoming events count
*/
public function test_upcoming_events_count() {
// Create past event
$this->create_test_event( array(
'meta' => array(
'_EventStartDate' => date( 'Y-m-d H:i:s', strtotime( '-1 week' ) ),
'_EventEndDate' => date( 'Y-m-d H:i:s', strtotime( '-1 week +2 hours' ) ),
),
) );
// Create future event
$this->create_test_event( array(
'meta' => array(
'_EventStartDate' => date( 'Y-m-d H:i:s', strtotime( '+1 week' ) ),
'_EventEndDate' => date( 'Y-m-d H:i:s', strtotime( '+1 week +2 hours' ) ),
),
) );
// Create current event
$this->create_test_event( array(
'meta' => array(
'_EventStartDate' => date( 'Y-m-d H:i:s', strtotime( '-1 hour' ) ),
'_EventEndDate' => date( 'Y-m-d H:i:s', strtotime( '+1 hour' ) ),
),
) );
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
$stats = $dashboard_data->get_all_stats();
// Only the future event should count as upcoming
$this->assertEquals( 1, $stats['upcoming_events'] );
$this->assertEquals( 1, $stats['past_events'] );
}
/**
* Test events table data
*/
public function test_get_events_table_data() {
// Create various status events
$this->create_test_event( array(
'post_status' => 'publish',
'post_title' => 'Published Event',
) );
$this->create_test_event( array(
'post_status' => 'draft',
'post_title' => 'Draft Event',
) );
$this->create_test_event( array(
'post_status' => 'future',
'post_title' => 'Scheduled Event',
) );
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
// Get all events
$all_events = $dashboard_data->get_events_table_data( 'all' );
$this->assertCount( 3, $all_events );
// Get only published events
$published_events = $dashboard_data->get_events_table_data( 'publish' );
$this->assertCount( 1, $published_events );
$this->assertEquals( 'publish', $published_events[0]['status'] );
// Get only draft events
$draft_events = $dashboard_data->get_events_table_data( 'draft' );
$this->assertCount( 1, $draft_events );
$this->assertEquals( 'draft', $draft_events[0]['status'] );
// Verify event data structure
$event = $all_events[0];
$this->assertArrayHasKey( 'id', $event );
$this->assertArrayHasKey( 'status', $event );
$this->assertArrayHasKey( 'name', $event );
$this->assertArrayHasKey( 'link', $event );
$this->assertArrayHasKey( 'start_date_ts', $event );
$this->assertArrayHasKey( 'organizer_id', $event );
$this->assertArrayHasKey( 'capacity', $event );
$this->assertArrayHasKey( 'sold', $event );
$this->assertArrayHasKey( 'revenue', $event );
}
/**
* Test revenue calculation
*/
public function test_revenue_calculation() {
// Create events with different revenue
$this->create_test_event( array(
'meta' => array(
'_tribe_revenue_total' => 1000.50,
),
) );
$this->create_test_event( array(
'meta' => array(
'_tribe_revenue_total' => 2500.75,
),
) );
$this->create_test_event( array(
'meta' => array(
'_tribe_revenue_total' => 0,
),
) );
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
$stats = $dashboard_data->get_all_stats();
$this->assertEquals( 3501.25, $stats['total_revenue'] );
}
/**
* Test tickets sold calculation
*/
public function test_tickets_sold_calculation() {
// Create events with different ticket counts
$this->create_test_event( array(
'meta' => array(
'_tribe_tickets_sold' => 10,
),
) );
$this->create_test_event( array(
'meta' => array(
'_tribe_tickets_sold' => 25,
),
) );
$this->create_test_event( array(
'meta' => array(
'_tribe_tickets_sold' => 0,
),
) );
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
$stats = $dashboard_data->get_all_stats();
$this->assertEquals( 35, $stats['total_tickets'] );
}
/**
* Test with no events
*/
public function test_with_no_events() {
$dashboard_data = new HVAC_Dashboard_Data_Refactored( $this->user_id );
$stats = $dashboard_data->get_all_stats();
$this->assertEquals( 0, $stats['total_events'] );
$this->assertEquals( 0, $stats['upcoming_events'] );
$this->assertEquals( 0, $stats['past_events'] );
$this->assertEquals( 0, $stats['total_tickets'] );
$this->assertEquals( 0, $stats['total_revenue'] );
$this->assertNull( $stats['revenue_target'] );
$events = $dashboard_data->get_events_table_data();
$this->assertIsArray( $events );
$this->assertEmpty( $events );
}
}

View file

@ -0,0 +1,324 @@
<?php
/**
* Tests for HVAC_Form_Builder class
*
* @package HVAC_Community_Events
* @subpackage Tests
*/
/**
* Class HVAC_Form_Builder_Test
*/
class HVAC_Form_Builder_Test extends WP_UnitTestCase {
/**
* Test form builder initialization
*/
public function test_form_builder_initialization() {
$form = new HVAC_Form_Builder( 'test_nonce' );
$this->assertInstanceOf( 'HVAC_Form_Builder', $form );
}
/**
* Test adding fields
*/
public function test_add_field() {
$form = new HVAC_Form_Builder( 'test_nonce' );
// Add text field
$form->add_field( array(
'type' => 'text',
'name' => 'test_field',
'label' => 'Test Field',
'required' => true,
) );
// Add email field
$form->add_field( array(
'type' => 'email',
'name' => 'email_field',
'label' => 'Email Field',
'validate' => array( 'email' => true ),
) );
// Test that fields were added (we'd need getter methods to properly test this)
$this->assertTrue( true );
}
/**
* Test form rendering
*/
public function test_render_form() {
$form = new HVAC_Form_Builder( 'test_nonce' );
// Add a simple field
$form->add_field( array(
'type' => 'text',
'name' => 'test_field',
'label' => 'Test Field',
) );
// Set form attributes
$form->set_attributes( array(
'id' => 'test-form',
'class' => 'test-form-class',
) );
// Render form
$output = $form->render();
// Verify output contains expected elements
$this->assertStringContainsString( '<form', $output );
$this->assertStringContainsString( 'id="test-form"', $output );
$this->assertStringContainsString( 'class="test-form-class hvac-form"', $output );
$this->assertStringContainsString( 'Test Field', $output );
$this->assertStringContainsString( 'name="test_field"', $output );
$this->assertStringContainsString( 'wp_nonce', $output );
}
/**
* Test different field types
*/
public function test_field_types() {
$form = new HVAC_Form_Builder( 'test_nonce' );
// Add various field types
$form->add_field( array(
'type' => 'text',
'name' => 'text_field',
'label' => 'Text Field',
) );
$form->add_field( array(
'type' => 'email',
'name' => 'email_field',
'label' => 'Email Field',
) );
$form->add_field( array(
'type' => 'textarea',
'name' => 'textarea_field',
'label' => 'Textarea Field',
) );
$form->add_field( array(
'type' => 'select',
'name' => 'select_field',
'label' => 'Select Field',
'options' => array(
'option1' => 'Option 1',
'option2' => 'Option 2',
),
) );
$form->add_field( array(
'type' => 'checkbox',
'name' => 'checkbox_field',
'label' => 'Checkbox Field',
) );
$form->add_field( array(
'type' => 'radio',
'name' => 'radio_field',
'label' => 'Radio Field',
'options' => array(
'option1' => 'Option 1',
'option2' => 'Option 2',
),
) );
$form->add_field( array(
'type' => 'file',
'name' => 'file_field',
'label' => 'File Field',
) );
$output = $form->render();
// Verify all field types are rendered
$this->assertStringContainsString( 'type="text"', $output );
$this->assertStringContainsString( 'type="email"', $output );
$this->assertStringContainsString( '<textarea', $output );
$this->assertStringContainsString( '<select', $output );
$this->assertStringContainsString( 'type="checkbox"', $output );
$this->assertStringContainsString( 'type="radio"', $output );
$this->assertStringContainsString( 'type="file"', $output );
$this->assertStringContainsString( 'enctype="multipart/form-data"', $output );
}
/**
* Test form validation
*/
public function test_validation() {
$form = new HVAC_Form_Builder( 'test_nonce' );
// Add fields with validation rules
$form->add_field( array(
'type' => 'text',
'name' => 'required_field',
'label' => 'Required Field',
'required' => true,
) );
$form->add_field( array(
'type' => 'email',
'name' => 'email_field',
'label' => 'Email Field',
'required' => true,
'validate' => array( 'email' => true ),
) );
$form->add_field( array(
'type' => 'text',
'name' => 'min_length_field',
'label' => 'Min Length Field',
'validate' => array( 'min_length' => 5 ),
) );
// Test with empty data
$errors = $form->validate( array() );
$this->assertArrayHasKey( 'required_field', $errors );
$this->assertArrayHasKey( 'email_field', $errors );
// Test with invalid email
$errors = $form->validate( array(
'required_field' => 'value',
'email_field' => 'invalid-email',
) );
$this->assertArrayHasKey( 'email_field', $errors );
// Test with short value
$errors = $form->validate( array(
'required_field' => 'value',
'email_field' => 'test@example.com',
'min_length_field' => 'abc',
) );
$this->assertArrayHasKey( 'min_length_field', $errors );
// Test with valid data
$errors = $form->validate( array(
'required_field' => 'value',
'email_field' => 'test@example.com',
'min_length_field' => 'long enough',
) );
$this->assertEmpty( $errors );
}
/**
* Test form sanitization
*/
public function test_sanitization() {
$form = new HVAC_Form_Builder( 'test_nonce' );
// Add fields with different sanitization types
$form->add_field( array(
'type' => 'text',
'name' => 'text_field',
'sanitize' => 'text',
) );
$form->add_field( array(
'type' => 'email',
'name' => 'email_field',
'sanitize' => 'email',
) );
$form->add_field( array(
'type' => 'url',
'name' => 'url_field',
'sanitize' => 'url',
) );
$form->add_field( array(
'type' => 'textarea',
'name' => 'textarea_field',
'sanitize' => 'textarea',
) );
$form->add_field( array(
'type' => 'number',
'name' => 'int_field',
'sanitize' => 'int',
) );
// Test sanitization
$raw_data = array(
'text_field' => '<script>alert("xss")</script>',
'email_field' => ' TEST@EXAMPLE.COM ',
'url_field' => 'HTTPS://EXAMPLE.COM',
'textarea_field' => "Line 1\nLine 2\n<script>alert('xss')</script>",
'int_field' => '123abc',
);
$sanitized = $form->sanitize( $raw_data );
$this->assertEquals( 'alert(&quot;xss&quot;)', $sanitized['text_field'] );
$this->assertEquals( 'test@example.com', $sanitized['email_field'] );
$this->assertEquals( 'https://EXAMPLE.COM', $sanitized['url_field'] );
$this->assertStringContainsString( 'Line 1', $sanitized['textarea_field'] );
$this->assertStringNotContainsString( '<script>', $sanitized['textarea_field'] );
$this->assertEquals( 123, $sanitized['int_field'] );
}
/**
* Test form with errors display
*/
public function test_form_with_errors() {
$form = new HVAC_Form_Builder( 'test_nonce' );
// Add field
$form->add_field( array(
'type' => 'text',
'name' => 'test_field',
'label' => 'Test Field',
'required' => true,
) );
// Set errors
$form->set_errors( array(
'test_field' => 'This field is required',
) );
$output = $form->render();
// Verify error is displayed
$this->assertStringContainsString( 'This field is required', $output );
$this->assertStringContainsString( 'class="error"', $output );
}
/**
* Test form with existing data
*/
public function test_form_with_data() {
$form = new HVAC_Form_Builder( 'test_nonce' );
// Add fields
$form->add_field( array(
'type' => 'text',
'name' => 'text_field',
'label' => 'Text Field',
) );
$form->add_field( array(
'type' => 'select',
'name' => 'select_field',
'label' => 'Select Field',
'options' => array(
'option1' => 'Option 1',
'option2' => 'Option 2',
),
) );
// Set data
$form->set_data( array(
'text_field' => 'Existing Value',
'select_field' => 'option2',
) );
$output = $form->render();
// Verify values are populated
$this->assertStringContainsString( 'value="Existing Value"', $output );
$this->assertStringContainsString( 'value="option2" selected', $output );
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* Tests for HVAC_Logger class
*
* @package HVAC_Community_Events
* @subpackage Tests
*/
/**
* Class HVAC_Logger_Test
*/
class HVAC_Logger_Test extends WP_UnitTestCase {
/**
* Test logger initialization
*/
public function test_logger_initialization() {
// Reset logger state
$reflection = new ReflectionClass( 'HVAC_Logger' );
$enabled_property = $reflection->getProperty( 'enabled' );
$enabled_property->setAccessible( true );
$enabled_property->setValue( null, null );
// Test initialization
HVAC_Logger::init();
// Logger should be disabled by default in test environment
$this->assertNotNull( $enabled_property->getValue() );
}
/**
* Test logging when disabled
*/
public function test_logging_when_disabled() {
HVAC_Logger::set_enabled( false );
// This should not produce any output
$this->expectOutputString( '' );
HVAC_Logger::log( 'Test message' );
}
/**
* Test logging when enabled
*/
public function test_logging_when_enabled() {
// Enable logging
HVAC_Logger::set_enabled( true );
// Capture error log output
$this->setOutputCallback( function( $output ) {
return ''; // Suppress output for tests
} );
// Log a message
HVAC_Logger::log( 'Test message', 'TestContext' );
// We can't easily test error_log output, but we can verify the method executes
$this->assertTrue( true );
}
/**
* Test different log levels
*/
public function test_log_levels() {
HVAC_Logger::set_enabled( true );
// Test error logging
HVAC_Logger::error( 'Test error', 'TestContext' );
// Test warning logging
HVAC_Logger::warning( 'Test warning', 'TestContext' );
// Test info logging
HVAC_Logger::info( 'Test info', 'TestContext' );
// If no exceptions thrown, test passes
$this->assertTrue( true );
}
/**
* Test logging with data
*/
public function test_logging_with_data() {
HVAC_Logger::set_enabled( true );
$test_data = array(
'user_id' => 123,
'action' => 'test_action',
);
HVAC_Logger::log( 'Test with data', 'TestContext', $test_data );
// If no exceptions thrown, test passes
$this->assertTrue( true );
}
/**
* Test debug nonce generation
*/
public function test_debug_nonce() {
$nonce = HVAC_Logger::get_debug_nonce();
$this->assertNotEmpty( $nonce );
$this->assertIsString( $nonce );
// Verify nonce is valid
$this->assertNotFalse( wp_verify_nonce( $nonce, 'hvac_debug_nonce' ) );
}
/**
* Test enable/disable persistence
*/
public function test_enable_disable_persistence() {
// Enable logging
HVAC_Logger::set_enabled( true );
$option_value = get_option( 'hvac_ce_debug_mode' );
$this->assertTrue( $option_value );
// Disable logging
HVAC_Logger::set_enabled( false );
$option_value = get_option( 'hvac_ce_debug_mode' );
$this->assertFalse( $option_value );
}
}

View file

@ -0,0 +1,226 @@
<?php
/**
* Tests for HVAC_Security class
*
* @package HVAC_Community_Events
* @subpackage Tests
*/
/**
* Class HVAC_Security_Test
*/
class HVAC_Security_Test extends WP_UnitTestCase {
/**
* Setup test
*/
public function setUp(): void {
parent::setUp();
wp_set_current_user( 0 ); // Start with no user
}
/**
* Test nonce verification
*/
public function test_verify_nonce() {
$action = 'test_action';
$nonce = wp_create_nonce( $action );
// Test valid nonce
$result = HVAC_Security::verify_nonce( $nonce, $action );
$this->assertTrue( $result );
// Test invalid nonce
$result = HVAC_Security::verify_nonce( 'invalid_nonce', $action );
$this->assertFalse( $result );
// Test with die_on_fail
$this->expectException( 'WPDieException' );
HVAC_Security::verify_nonce( 'invalid_nonce', $action, true );
}
/**
* Test capability check
*/
public function test_check_capability() {
// Create test user with specific capability
$user_id = $this->factory->user->create( array(
'role' => 'administrator',
) );
wp_set_current_user( $user_id );
// Test valid capability
$result = HVAC_Security::check_capability( 'manage_options' );
$this->assertTrue( $result );
// Test invalid capability
$result = HVAC_Security::check_capability( 'non_existent_capability' );
$this->assertFalse( $result );
// Test with die_on_fail
$this->expectException( 'WPDieException' );
HVAC_Security::check_capability( 'non_existent_capability', true );
}
/**
* Test login requirement
*/
public function test_require_login() {
// Test when not logged in
wp_set_current_user( 0 );
$result = HVAC_Security::require_login();
$this->assertFalse( $result );
// Test when logged in
$user_id = $this->factory->user->create();
wp_set_current_user( $user_id );
$result = HVAC_Security::require_login();
$this->assertTrue( $result );
}
/**
* Test email sanitization
*/
public function test_sanitize_email() {
// Valid email
$email = HVAC_Security::sanitize_email( 'test@example.com' );
$this->assertEquals( 'test@example.com', $email );
// Invalid email
$email = HVAC_Security::sanitize_email( 'invalid-email' );
$this->assertFalse( $email );
// Email with spaces
$email = HVAC_Security::sanitize_email( ' test@example.com ' );
$this->assertEquals( 'test@example.com', $email );
// XSS attempt
$email = HVAC_Security::sanitize_email( '<script>alert("xss")</script>test@example.com' );
$this->assertFalse( $email );
}
/**
* Test URL sanitization
*/
public function test_sanitize_url() {
// Valid URL
$url = HVAC_Security::sanitize_url( 'https://example.com/path' );
$this->assertEquals( 'https://example.com/path', $url );
// Invalid URL
$url = HVAC_Security::sanitize_url( 'not a url' );
$this->assertFalse( $url );
// JavaScript URL
$url = HVAC_Security::sanitize_url( 'javascript:alert("xss")' );
$this->assertFalse( $url );
// URL with spaces
$url = HVAC_Security::sanitize_url( ' https://example.com ' );
$this->assertEquals( 'https://example.com', $url );
}
/**
* Test array sanitization
*/
public function test_sanitize_array() {
// Text array
$input = array( 'one', '<script>alert("xss")</script>', 'three' );
$result = HVAC_Security::sanitize_array( $input, 'text' );
$this->assertEquals( array( 'one', 'alert(&quot;xss&quot;)', 'three' ), $result );
// Email array
$input = array( 'test@example.com', 'invalid', 'admin@example.com' );
$result = HVAC_Security::sanitize_array( $input, 'email' );
$this->assertEquals( array( 0 => 'test@example.com', 2 => 'admin@example.com' ), $result );
// URL array
$input = array( 'https://example.com', 'javascript:alert()', 'https://test.com' );
$result = HVAC_Security::sanitize_array( $input, 'url' );
$this->assertEquals( array( 0 => 'https://example.com', 2 => 'https://test.com' ), $result );
// Integer array
$input = array( '123', '456abc', '789' );
$result = HVAC_Security::sanitize_array( $input, 'int' );
$this->assertEquals( array( 123, 456, 789 ), $result );
}
/**
* Test output escaping
*/
public function test_escape_output() {
$input = '<script>alert("xss")</script>';
// HTML context
$result = HVAC_Security::escape_output( $input, 'html' );
$this->assertEquals( '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;', $result );
// Attribute context
$result = HVAC_Security::escape_output( $input, 'attr' );
$this->assertEquals( '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;', $result );
// URL context
$url = 'https://example.com?param=<script>';
$result = HVAC_Security::escape_output( $url, 'url' );
$this->assertEquals( 'https://example.com?param=%3Cscript%3E', $result );
// JavaScript context
$result = HVAC_Security::escape_output( $input, 'js' );
$this->assertStringContainsString( '\\x3c', $result ); // JS escaping
}
/**
* Test AJAX request detection
*/
public function test_is_ajax_request() {
// Not AJAX by default
$this->assertFalse( HVAC_Security::is_ajax_request() );
// Simulate AJAX request
define( 'DOING_AJAX', true );
$this->assertTrue( HVAC_Security::is_ajax_request() );
}
/**
* Test rate limiting
*/
public function test_check_rate_limit() {
$action = 'test_action';
$limit = 3;
$window = 60;
// First attempts should succeed
for ( $i = 0; $i < $limit; $i++ ) {
$result = HVAC_Security::check_rate_limit( $action, $limit, $window );
$this->assertTrue( $result );
}
// Next attempt should fail
$result = HVAC_Security::check_rate_limit( $action, $limit, $window );
$this->assertFalse( $result );
// Different action should work
$result = HVAC_Security::check_rate_limit( 'different_action', $limit, $window );
$this->assertTrue( $result );
}
/**
* Test get user IP
*/
public function test_get_user_ip() {
// Test with REMOTE_ADDR
$_SERVER['REMOTE_ADDR'] = '192.168.1.1';
$ip = HVAC_Security::get_user_ip();
$this->assertEquals( '192.168.1.1', $ip );
// Test with HTTP_X_FORWARDED_FOR
$_SERVER['HTTP_X_FORWARDED_FOR'] = '10.0.0.1';
$ip = HVAC_Security::get_user_ip();
$this->assertEquals( '10.0.0.1', $ip );
// Test with HTTP_CLIENT_IP (highest priority)
$_SERVER['HTTP_CLIENT_IP'] = '172.16.0.1';
$ip = HVAC_Security::get_user_ip();
$this->assertEquals( '172.16.0.1', $ip );
}
}