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:
parent
5d45ed594d
commit
0e8b0f0325
41 changed files with 6941 additions and 50 deletions
|
|
@ -1,12 +1,50 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Load environment variables from .env
|
# 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
|
# Define deployment variables
|
||||||
REMOTE_HOST="$UPSKILL_STAGING_IP"
|
REMOTE_HOST="$UPSKILL_STAGING_IP"
|
||||||
REMOTE_USER="$UPSKILL_STAGING_SSH_USER"
|
REMOTE_USER="$UPSKILL_STAGING_SSH_USER"
|
||||||
REMOTE_PATH_BASE="$UPSKILL_STAGING_PATH"
|
REMOTE_PATH_BASE="$UPSKILL_STAGING_PATH"
|
||||||
PLUGIN_SLUG="hvac-community-events"
|
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"
|
||||||
|
)
|
||||||
91
wordpress-dev/bin/disable-breeze-cache-testing.sh
Executable file
91
wordpress-dev/bin/disable-breeze-cache-testing.sh
Executable 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"
|
||||||
61
wordpress-dev/bin/test-zoho-integration.sh
Executable file
61
wordpress-dev/bin/test-zoho-integration.sh
Executable 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}"
|
||||||
34
wordpress-dev/bin/zoho-oauth-setup.sh
Executable file
34
wordpress-dev/bin/zoho-oauth-setup.sh
Executable 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}"
|
||||||
60
wordpress-dev/bin/zoho-setup-complete.sh
Executable file
60
wordpress-dev/bin/zoho-setup-complete.sh
Executable 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!"
|
||||||
10
wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore
vendored
Normal file
10
wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Ignore Zoho credentials
|
||||||
|
includes/zoho/zoho-config.php
|
||||||
|
|
||||||
|
# Ignore log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
vendor/
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
|
@ -44,7 +44,7 @@ function hvac_ce_create_required_pages() {
|
||||||
],
|
],
|
||||||
'hvac-dashboard' => [
|
'hvac-dashboard' => [
|
||||||
'title' => 'Trainer 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
|
'manage-event' => [ // New page for TEC CE submission form shortcode
|
||||||
'title' => 'Manage Event',
|
'title' => 'Manage Event',
|
||||||
|
|
@ -217,4 +217,5 @@ function hvac_ce_include_order_summary_template( $template ) {
|
||||||
}
|
}
|
||||||
return $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 );
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
?>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,7 +54,10 @@ class HVAC_Community_Events {
|
||||||
'class-hvac-settings.php',
|
'class-hvac-settings.php',
|
||||||
'community/class-login-handler.php',
|
'community/class-login-handler.php',
|
||||||
'community/class-event-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) {
|
foreach ($files_to_include as $file) {
|
||||||
$path = HVAC_CE_PLUGIN_DIR . 'includes/' . $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');
|
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');
|
HVAC_Logger::info('All required files loaded', 'Core');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,8 +93,8 @@ class HVAC_Community_Events {
|
||||||
// Initialize other hooks
|
// Initialize other hooks
|
||||||
add_action('init', array($this, 'init'));
|
add_action('init', array($this, 'init'));
|
||||||
|
|
||||||
// Template loading for custom pages
|
// Template loading for custom pages (removed - using content filter instead)
|
||||||
add_filter('template_include', array($this, 'load_custom_templates'));
|
// add_filter('template_include', array($this, 'load_custom_templates'));
|
||||||
} // End init_hooks
|
} // End init_hooks
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -98,62 +111,121 @@ class HVAC_Community_Events {
|
||||||
public static function deactivate() {
|
public static function deactivate() {
|
||||||
// Remove the hvac_trainer role
|
// Remove the hvac_trainer role
|
||||||
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php'; // Ensure class is available
|
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php'; // Ensure class is available
|
||||||
$roles = new HVAC_Roles();
|
HVAC_Roles::remove_hvac_trainer_role();
|
||||||
$roles->remove_trainer_role();
|
HVAC_Logger::info('Deactivation completed: HVAC trainer role removed.', 'Core');
|
||||||
|
|
||||||
// Additional deactivation tasks
|
|
||||||
// ...
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize plugin actions attached to 'init' hook
|
* Initialize function (hooked on 'init')
|
||||||
*/
|
*/
|
||||||
public function init() {
|
public function init() {
|
||||||
HVAC_Logger::info('Init method started', 'Core');
|
// Initialize roles
|
||||||
// Initialize handlers
|
$this->init_roles();
|
||||||
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
|
// Initialize forms
|
||||||
add_action('admin_init', array($this, 'redirect_trainers_from_admin'));
|
$this->init_forms();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Initialize shortcodes
|
||||||
* Redirect HVAC trainers from admin area to frontend dashboard
|
$this->init_shortcodes();
|
||||||
*/
|
|
||||||
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
|
// Initialize event form handler
|
||||||
if ( is_admin() && ! current_user_can('manage_options') && current_user_can('view_hvac_dashboard') ) {
|
if (class_exists('HVAC_Community_Events\Event_Form_Handler')) {
|
||||||
wp_redirect(home_url('/hvac-dashboard/')); // Corrected slug
|
new \HVAC_Community_Events\Event_Form_Handler();
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load custom templates for plugin pages.
|
* Initialize roles
|
||||||
*
|
|
||||||
* @param string $template The path of the template to include.
|
|
||||||
* @return string The path of the template to include.
|
|
||||||
*/
|
*/
|
||||||
public function load_custom_templates( $template ) {
|
private function init_roles() {
|
||||||
// Check if we are on the HVAC Dashboard page
|
$roles = new HVAC_Roles();
|
||||||
if ( is_page( 'hvac-dashboard' ) ) {
|
// Note: Role creation is handled in the activate method or the class constructor
|
||||||
$new_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php';
|
}
|
||||||
if ( file_exists( $new_template ) ) {
|
|
||||||
return $new_template;
|
/**
|
||||||
|
* 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;
|
return $template;
|
||||||
}
|
} // End load_custom_templates
|
||||||
|
|
||||||
|
|
||||||
} // End class HVAC_Community_Events
|
} // End class HVAC_Community_Events
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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');
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
?>
|
||||||
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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("xss")', $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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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("xss")', '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( '<script>alert("xss")</script>', $result );
|
||||||
|
|
||||||
|
// Attribute context
|
||||||
|
$result = HVAC_Security::escape_output( $input, 'attr' );
|
||||||
|
$this->assertEquals( '<script>alert("xss")</script>', $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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue