From 0e8b0f032563202ab25c85584eaa45d44b06e854 Mon Sep 17 00:00:00 2001 From: bengizmo Date: Mon, 19 May 2025 13:17:44 -0300 Subject: [PATCH] feat: Add Zoho CRM integration with staging mode protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- wordpress-dev/bin/deploy_config.sh | 44 +- .../bin/disable-breeze-cache-testing.sh | 91 ++++ wordpress-dev/bin/test-zoho-integration.sh | 61 +++ wordpress-dev/bin/zoho-oauth-setup.sh | 34 ++ wordpress-dev/bin/zoho-setup-complete.sh | 60 +++ .../plugins/hvac-community-events/.gitignore | 10 + .../assets/css/zoho-admin.css | 56 ++ .../assets/js/zoho-admin.js | 128 +++++ .../hvac-community-events/bin/run-tests.sh | 54 ++ .../hvac-community-events.php | 5 +- .../includes/admin/class-zoho-admin.php | 231 ++++++++ .../includes/class-event-author-fixer.php | 96 ++++ .../includes/class-event-form-handler.php | 55 ++ .../includes/class-hvac-community-events.php | 162 ++++-- .../class-hvac-dashboard-data-fixed.php | 243 +++++++++ .../class-hvac-dashboard-data-refactored.php | 335 ++++++++++++ .../includes/class-hvac-dashboard.php | 394 ++++++++++++++ .../includes/class-hvac-form-builder.php | 501 ++++++++++++++++++ .../includes/class-hvac-logger.php | 156 ++++++ .../includes/class-hvac-security.php | 231 ++++++++ .../class-hvac-settings-refactored.php | 411 ++++++++++++++ .../includes/zoho/README.md | 134 +++++ .../includes/zoho/STAGING-MODE.md | 95 ++++ .../includes/zoho/TESTING.md | 97 ++++ .../includes/zoho/auth-server.php | 57 ++ .../includes/zoho/class-zoho-admin.php | 211 ++++++++ .../includes/zoho/class-zoho-crm-auth.php | 262 +++++++++ .../includes/zoho/class-zoho-sync.php | 428 +++++++++++++++ .../includes/zoho/setup-helper.php | 155 ++++++ .../includes/zoho/test-integration.php | 217 ++++++++ .../includes/zoho/zoho-config-template.php | 33 ++ .../plugins/hvac-community-events/phpunit.xml | 39 ++ .../hvac-community-events/refactoring-plan.md | 86 +++ .../tests/integration/test-dashboard-flow.php | 230 ++++++++ .../test-event-submission-flow.php | 298 +++++++++++ .../tests/setup-test-events.php | 209 ++++++++ .../tests/test-zoho-staging-mode.php | 68 +++ .../class-hvac-dashboard-data-test.php | 340 ++++++++++++ .../class-hvac-form-builder-test.php | 324 +++++++++++ .../unit/logger/class-hvac-logger-test.php | 124 +++++ .../security/class-hvac-security-test.php | 226 ++++++++ 41 files changed, 6941 insertions(+), 50 deletions(-) create mode 100755 wordpress-dev/bin/disable-breeze-cache-testing.sh create mode 100755 wordpress-dev/bin/test-zoho-integration.sh create mode 100755 wordpress-dev/bin/zoho-oauth-setup.sh create mode 100755 wordpress-dev/bin/zoho-setup-complete.sh create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/zoho-admin.js create mode 100755 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/bin/run-tests.sh create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/admin/class-zoho-admin.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-author-fixer.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-form-handler.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-fixed.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-refactored.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-form-builder.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-logger.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-security.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-settings-refactored.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/README.md create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/STAGING-MODE.md create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/TESTING.md create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/auth-server.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-admin.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-crm-auth.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-sync.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/setup-helper.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/test-integration.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/zoho-config-template.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/phpunit.xml create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/refactoring-plan.md create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-dashboard-flow.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-event-submission-flow.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/setup-test-events.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/test-zoho-staging-mode.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/dashboard-data/class-hvac-dashboard-data-test.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/form-builder/class-hvac-form-builder-test.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/logger/class-hvac-logger-test.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/security/class-hvac-security-test.php diff --git a/wordpress-dev/bin/deploy_config.sh b/wordpress-dev/bin/deploy_config.sh index 447a1ad6..537b032b 100755 --- a/wordpress-dev/bin/deploy_config.sh +++ b/wordpress-dev/bin/deploy_config.sh @@ -1,12 +1,50 @@ #!/bin/bash # Load environment variables from .env -source ../.env +if [ -f "$(dirname "$0")/../../.env" ]; then + source "$(dirname "$0")/../../.env" +elif [ -f ".env" ]; then + source ".env" +else + echo "Error: .env file not found" + exit 1 +fi # Define deployment variables REMOTE_HOST="$UPSKILL_STAGING_IP" REMOTE_USER="$UPSKILL_STAGING_SSH_USER" REMOTE_PATH_BASE="$UPSKILL_STAGING_PATH" PLUGIN_SLUG="hvac-community-events" -REMOTE_PLUGIN_PATH="$REMOTE_PATH_BASE/wp-content/plugins/$PLUGIN_SLUG" -LOCAL_PLUGIN_PATH="wordpress-dev/wordpress/wp-content/plugins/$PLUGIN_SLUG" \ No newline at end of file + +# 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" +) \ No newline at end of file diff --git a/wordpress-dev/bin/disable-breeze-cache-testing.sh b/wordpress-dev/bin/disable-breeze-cache-testing.sh new file mode 100755 index 00000000..e6ff2aad --- /dev/null +++ b/wordpress-dev/bin/disable-breeze-cache-testing.sh @@ -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=' 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" \ No newline at end of file diff --git a/wordpress-dev/bin/test-zoho-integration.sh b/wordpress-dev/bin/test-zoho-integration.sh new file mode 100755 index 00000000..6770ede6 --- /dev/null +++ b/wordpress-dev/bin/test-zoho-integration.sh @@ -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}" \ No newline at end of file diff --git a/wordpress-dev/bin/zoho-oauth-setup.sh b/wordpress-dev/bin/zoho-oauth-setup.sh new file mode 100755 index 00000000..e88083bc --- /dev/null +++ b/wordpress-dev/bin/zoho-oauth-setup.sh @@ -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}" \ No newline at end of file diff --git a/wordpress-dev/bin/zoho-setup-complete.sh b/wordpress-dev/bin/zoho-setup-complete.sh new file mode 100755 index 00000000..a55dd3db --- /dev/null +++ b/wordpress-dev/bin/zoho-setup-complete.sh @@ -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!" \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore new file mode 100644 index 00000000..2ad4b848 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore @@ -0,0 +1,10 @@ +# Ignore Zoho credentials +includes/zoho/zoho-config.php + +# Ignore log files +*.log + +# Development files +.DS_Store +node_modules/ +vendor/ \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css new file mode 100644 index 00000000..f429fce3 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css @@ -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; +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/zoho-admin.js b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/zoho-admin.js new file mode 100644 index 00000000..7d7a53c2 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/zoho-admin.js @@ -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('

' + response.data.message + ' (' + response.data.modules + ')

'); + } else { + $status.html('

' + response.data.message + ': ' + response.data.error + '

'); + } + }, + error: function() { + $status.html('

Connection test failed

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

Syncing ' + type + '...

'); + + $.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 = '
'; + + if (result.staging_mode) { + html += '

🔧 STAGING MODE - Simulation Results

'; + html += '

' + result.message + '

'; + } else { + html += '

Sync completed successfully!

'; + } + + html += ''; + + if (result.test_data && result.test_data.length > 0) { + html += '
' + + 'View test data (first 5 records)' + + '
' +
+                            JSON.stringify(result.test_data.slice(0, 5), null, 2) +
+                            '
' + + '
'; + } + + html += '
'; + $status.html(html); + } else { + $status.html('

' + response.data.message + ': ' + response.data.error + '

'); + } + }, + error: function() { + $status.html('

Sync failed

'); + }, + 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'); + } + }); + }); +}); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/bin/run-tests.sh b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/bin/run-tests.sh new file mode 100755 index 00000000..ad8c1753 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/bin/run-tests.sh @@ -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 \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php index bc2afc64..6d23eed7 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php @@ -44,7 +44,7 @@ function hvac_ce_create_required_pages() { ], 'hvac-dashboard' => [ 'title' => 'Trainer Dashboard', - 'content' => '', // Content handled by template or redirect + 'content' => '[hvac_trainer_dashboard]', ], 'manage-event' => [ // New page for TEC CE submission form shortcode 'title' => 'Manage Event', @@ -217,4 +217,5 @@ function hvac_ce_include_order_summary_template( $template ) { } return $template; } -add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 ); +// Removed - template handling is now in the main class +// add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 ); diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/admin/class-zoho-admin.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/admin/class-zoho-admin.php new file mode 100644 index 00000000..6e3bfc7e --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/admin/class-zoho-admin.php @@ -0,0 +1,231 @@ + 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; + ?> +
+

Zoho CRM Sync

+ + +
+

🔧 STAGING MODE ACTIVE

+

Current site:

+

Staging mode is active. Data sync will be simulated only. No actual data will be sent to Zoho CRM.

+

Production sync is only enabled on upskillhvac.com

+
+ + + +
+

Zoho CRM is not configured. Please complete the OAuth setup first.

+

Run: ./bin/zoho-setup-complete.sh

+
+ +
+

Connection Status

+ +
+
+ +
+

Data Sync

+ +
+

Events → Campaigns

+

Sync events from The Events Calendar to Zoho CRM Campaigns

+ +
+
+ +
+

Users → Contacts

+

Sync trainers and attendees to Zoho CRM Contacts

+ +
+
+ +
+

Purchases → Invoices

+

Sync ticket purchases to Zoho CRM Invoices

+ +
+
+
+ +
+

Sync Settings

+
+ +

+ +

+ +
+
+ +
+ 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(); +?> \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-author-fixer.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-author-fixer.php new file mode 100644 index 00000000..2132bf3a --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-author-fixer.php @@ -0,0 +1,96 @@ + 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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-form-handler.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-form-handler.php new file mode 100644 index 00000000..799ec479 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-form-handler.php @@ -0,0 +1,55 @@ +remove_trainer_role(); - - // Additional deactivation tasks - // ... + HVAC_Roles::remove_hvac_trainer_role(); + HVAC_Logger::info('Deactivation completed: HVAC trainer role removed.', 'Core'); } /** - * Initialize plugin actions attached to 'init' hook + * Initialize function (hooked on 'init') */ public function init() { - HVAC_Logger::info('Init method started', 'Core'); - // Initialize handlers - new \HVAC_Community_Events\Community\Login_Handler(); - HVAC_Logger::info('Login_Handler initialized', 'Core'); - new HVAC_Registration(); - HVAC_Logger::info('HVAC_Registration initialized', 'Core'); - HVAC_Logger::info('Init method completed', 'Core'); - - // Prevent trainers from accessing wp-admin - add_action('admin_init', array($this, 'redirect_trainers_from_admin')); - } - - /** - * Redirect HVAC trainers from admin area to frontend dashboard - */ - public function redirect_trainers_from_admin() { - if (defined('DOING_AJAX') && DOING_AJAX) { - return; - } - - // Check if user is trying to access wp-admin and has trainer role but not admin caps - if ( is_admin() && ! current_user_can('manage_options') && current_user_can('view_hvac_dashboard') ) { - wp_redirect(home_url('/hvac-dashboard/')); // Corrected slug - exit; + // Initialize roles + $this->init_roles(); + + // Initialize forms + $this->init_forms(); + + // Initialize shortcodes + $this->init_shortcodes(); + + // Initialize event form handler + if (class_exists('HVAC_Community_Events\Event_Form_Handler')) { + new \HVAC_Community_Events\Event_Form_Handler(); } } /** - * Load custom templates for plugin pages. - * - * @param string $template The path of the template to include. - * @return string The path of the template to include. + * Initialize roles */ - public function load_custom_templates( $template ) { - // Check if we are on the HVAC Dashboard page - if ( is_page( 'hvac-dashboard' ) ) { - $new_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php'; - if ( file_exists( $new_template ) ) { - return $new_template; + private function init_roles() { + $roles = new HVAC_Roles(); + // Note: Role creation is handled in the activate method or the class constructor + } + + /** + * Initialize forms + */ + private function init_forms() { + $registration = new HVAC_Registration(); + // Note: Form registration is handled in the class constructor + } + + /** + * Initialize shortcodes + */ + private function init_shortcodes() { + // Registration form shortcode + add_shortcode('hvac_trainer_registration', array('HVAC_Registration', 'render_registration_form')); + + // Community login shortcode + add_shortcode('hvac_community_login', array('HVAC_Community_Login_Handler', 'render_login_form')); + + // Dashboard shortcode + add_shortcode('hvac_dashboard', array($this, 'render_dashboard')); + + // Add the event summary shortcode + add_shortcode('hvac_event_summary', array($this, 'render_event_summary')); + + // Remove the event form shortcode as we're using TEC's shortcode instead + // add_shortcode('hvac_event_form', array('HVAC_Community_Event_Handler', 'render_event_form')); + + // Add future shortcodes here + } + + /** + * Render dashboard content + */ + public function render_dashboard() { + if (!is_user_logged_in()) { + return '

Please log in to view the dashboard.

'; + } + + // 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 '
Event Summary Content Here
'; + } + + /** + * Include custom templates for plugin pages + */ + public function load_custom_templates($template) { + // Check for dashboard page + if (is_page('hvac-dashboard')) { + $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php'; + if (file_exists($custom_template)) { + return $custom_template; } } - // Add checks for other custom pages here if needed + // Check for my-events page + if (is_page('my-events')) { + $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/page-my-events.php'; + if (file_exists($custom_template)) { + return $custom_template; + } + } + + // Check for single event view (temporary) + if ( is_singular( 'tribe_events' ) ) { + $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/single-tribe_events.php'; + if ( file_exists( $custom_template ) ) { + return $custom_template; + } + } + + // Add future custom templates here return $template; - } + } // End load_custom_templates + } // End class HVAC_Community_Events \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-fixed.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-fixed.php new file mode 100644 index 00000000..e5c3f496 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-fixed.php @@ -0,0 +1,243 @@ +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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-refactored.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-refactored.php new file mode 100644 index 00000000..93af2ae9 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-refactored.php @@ -0,0 +1,335 @@ +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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard.php new file mode 100644 index 00000000..787a8b21 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard.php @@ -0,0 +1,394 @@ + +

Please log in to view the dashboard.

+

Login

+ '; + } + + if (!current_user_can('view_hvac_dashboard')) { + return '
+

You do not have permission to view this dashboard.

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

Trainer Dashboard

+ +
+ + +
+

Your Stats

+
+ +
+

Total Events

+

+
+ + +
+

Upcoming Events

+

+
+ + +
+

Past Events

+

+
+ + +
+

Tickets Sold

+

+
+ + +
+

Total Revenue

+

$

+ + Target: $ + +
+
+
+ + +
+

Your Events

+ + +
+ Filter: + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusEvent NameDateOrganizerCapacitySoldRevenueActions
+ + $ + + Edit | + Summary +
+ +

No events found.

+ +
+
+
+ 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(); + ?> +
get_form_attributes(); ?>> + nonce_action, $this->nonce_action . '_nonce' ); ?> + + fields as $field ) : ?> + render_field( $field ); ?> + + +
+ +
+
+ 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( '
', esc_attr( $field['wrapper_class'] ) ); + + // Label + if ( ! empty( $field['label'] ) ) { + $output .= sprintf( + '', + esc_attr( $field['id'] ), + esc_html( $field['label'] ), + $field['required'] ? ' *' : '' + ); + } + + // 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( '%s', esc_html( $field['description'] ) ); + } + + // Error + if ( isset( $this->errors[ $field['name'] ] ) ) { + $output .= sprintf( + '%s', + esc_html( $this->errors[ $field['name'] ] ) + ); + } + + $output .= '
'; + 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( + '', + 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( + ''; + 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( + '', + 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( + '', + 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 = '
'; + + foreach ( $field['options'] as $option_value => $option_label ) { + $output .= sprintf( + '', + esc_attr( $field['name'] ), + esc_attr( $option_value ), + checked( $value, $option_value, false ), + esc_html( $option_label ) + ); + } + + $output .= '
'; + 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( + '', + 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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-logger.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-logger.php new file mode 100644 index 00000000..fa5bcd68 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-logger.php @@ -0,0 +1,156 @@ + $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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-settings-refactored.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-settings-refactored.php new file mode 100644 index 00000000..5e0526fb --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-settings-refactored.php @@ -0,0 +1,411 @@ +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' ); + ?> +
+

+
+ settings_group ); + do_settings_sections( $this->page_slug ); + submit_button( __( 'Save Settings', 'hvac-community-events' ) ); + ?> +
+
+ ' . __( 'Configure general plugin settings.', 'hvac-community-events' ) . '

'; + } + + /** + * Render registration section description + */ + public function render_section_registration() { + echo '

' . __( 'Configure trainer registration settings.', 'hvac-community-events' ) . '

'; + } + + /** + * Render checkbox field + * + * @param array $args Field arguments + */ + public function render_field_checkbox( $args ) { + $value = $this->get( $args['section'], $args['key'] ); + ?> + + /> + +

+ get( $args['section'], $args['key'] ); + ?> + + +

+ get( $args['section'], $args['key'] ); + ?> + + +

+ ! 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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/README.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/README.md new file mode 100644 index 00000000..68fbdc98 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/README.md @@ -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 \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/STAGING-MODE.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/STAGING-MODE.md new file mode 100644 index 00000000..4527c14d --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/STAGING-MODE.md @@ -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 \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/TESTING.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/TESTING.md new file mode 100644 index 00000000..cb1526d2 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/TESTING.md @@ -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 \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/auth-server.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/auth-server.php new file mode 100644 index 00000000..d94718f9 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/auth-server.php @@ -0,0 +1,57 @@ +"; + $response .= "

Authorization Successful!

"; + $response .= "

Authorization code received. You can close this window.

"; + $response .= "

Code: $auth_code

"; + $response .= "

Copy this code and paste it in the terminal.

"; + $response .= ""; + + 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 .= "

404 Not Found

"; + + fwrite($conn, $response); + fclose($conn); + } +} + +fclose($server); +echo "\nServer stopped.\n"; \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-admin.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-admin.php new file mode 100644 index 00000000..2f21675b --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-admin.php @@ -0,0 +1,211 @@ +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() { + ?> +
+

Zoho CRM Integration

+ + + + +
+

Zoho CRM configuration file not found. Please follow the setup instructions below.

+
+ +

Setup Instructions

+
    +
  1. + Register your application in Zoho: + Go to Zoho API Console +
  2. +
  3. Create a new Server-based Application
  4. +
  5. Set redirect URI to:
  6. +
  7. Copy your Client ID and Client Secret
  8. +
  9. Run the setup helper script from command line: +
    cd zoho
    +php setup-helper.php
    +
  10. +
+ + + + make_api_request('/crm/v2/org'); + $connected = !is_wp_error($org_info) && isset($org_info['org']); + ?> + + +
+

✓ Connected to Zoho CRM

+
+ +

Organization Information

+ + + + + + + + + + + + + +
Organization Name
Organization ID
Time Zone
+ +

Integration Status

+ display_integration_status(); ?> + +

Actions

+

+ Test Sync + Create Custom Fields +

+ + +
+

✗ Not connected to Zoho CRM

+
+ +

Reconnect to Zoho

+

Click the button below to authorize this application with Zoho CRM:

+

+ Connect to Zoho CRM +

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModuleFields ConfiguredLast SyncStatus
Campaigns (Events)check_custom_fields('Campaigns'); ?>
Contacts (Users)check_custom_fields('Contacts'); ?>
Invoices (Orders)check_custom_fields('Invoices'); ?>
+ Pending'; + } +} + +// Initialize admin interface +if (is_admin()) { + new HVAC_Zoho_Admin(); +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-crm-auth.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-crm-auth.php new file mode 100644 index 00000000..b8ba9e83 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-crm-auth.php @@ -0,0 +1,262 @@ +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); + } + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-sync.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-sync.php new file mode 100644 index 00000000..8523d543 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/class-zoho-sync.php @@ -0,0 +1,428 @@ +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 + ); + } +} +?> \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/setup-helper.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/setup-helper.php new file mode 100644 index 00000000..91c92e7c --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/setup-helper.php @@ -0,0 +1,155 @@ + $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 = " $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 = " + + + + tests/unit + + + tests/integration + + + + + + includes + + + vendor + tests + + + + + + + + \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/refactoring-plan.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/refactoring-plan.md new file mode 100644 index 00000000..1afe7220 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/refactoring-plan.md @@ -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 \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-dashboard-flow.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-dashboard-flow.php new file mode 100644 index 00000000..94ac3b63 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-dashboard-flow.php @@ -0,0 +1,230 @@ +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 ); + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-event-submission-flow.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-event-submission-flow.php new file mode 100644 index 00000000..9865e7c1 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-event-submission-flow.php @@ -0,0 +1,298 @@ +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 ); + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/setup-test-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/setup-test-events.php new file mode 100644 index 00000000..0e547dc2 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/setup-test-events.php @@ -0,0 +1,209 @@ +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"; +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/test-zoho-staging-mode.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/test-zoho-staging-mode.php new file mode 100644 index 00000000..6467a47d --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/test-zoho-staging-mode.php @@ -0,0 +1,68 @@ +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"; +?> \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/dashboard-data/class-hvac-dashboard-data-test.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/dashboard-data/class-hvac-dashboard-data-test.php new file mode 100644 index 00000000..9ccecf9a --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/dashboard-data/class-hvac-dashboard-data-test.php @@ -0,0 +1,340 @@ +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 ); + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/form-builder/class-hvac-form-builder-test.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/form-builder/class-hvac-form-builder-test.php new file mode 100644 index 00000000..3cffb234 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/form-builder/class-hvac-form-builder-test.php @@ -0,0 +1,324 @@ +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( '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( 'assertStringContainsString( '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' => '', + 'email_field' => ' TEST@EXAMPLE.COM ', + 'url_field' => 'HTTPS://EXAMPLE.COM', + 'textarea_field' => "Line 1\nLine 2\n", + '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( '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', '', '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 = ''; + + // 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=