Compare commits

..

No commits in common. "4fc6676e0c62ac539633c8767656cf6a288dad66" and "6bb957d7726ddbf439f3fe7f08c69d8f4b2cdcec" have entirely different histories.

12 changed files with 422 additions and 2190 deletions

76
.gitignore vendored
View file

@ -1,5 +1,5 @@
# Ignore everything by default
# *
*
!.gitignore
!.gitattributes
@ -28,8 +28,6 @@
!hvac-community-events.php
!/includes/
/includes/*
!/includes/admin/
!/includes/zoho/
!/includes/**/*.php
!/templates/
/templates/*
@ -97,14 +95,14 @@
!/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/**
# Test files
# **/test-results/
# **/playwright-report/
# **/.phpunit.result.cache
# **/node_modules/
# **/vendor/
# **/screenshots/
# **/videos/
# **/traces/
**/test-results/
**/playwright-report/
**/.phpunit.result.cache
**/node_modules/
**/vendor/
**/screenshots/
**/videos/
**/traces/
# Documentation
!/docs/
@ -179,25 +177,25 @@
!/wp-content/plugins/
# Security - Sensitive Files (CRITICAL SECURITY)
# .env
.env
.env.*
# *.env
# **/.env
# **/.env.*
*.env
**/.env
**/.env.*
.auth/
# **/.auth/
# **/zoho-config.php
# **/wp-config.php
# **/wp-tests-config*.php
**/.auth/
**/zoho-config.php
**/wp-config.php
**/wp-tests-config*.php
memory-bank/mcpServers.md
# **/*config*.php
# **/*secret*
# **/*password*
# **/*credential*
# **/*.key
# **/*.pem
# **/*.p12
# **/*.pfx
**/*config*.php
**/*secret*
**/*password*
**/*credential*
**/*.key
**/*.pem
**/*.p12
**/*.pfx
# Security Framework - Sensitive Runtime Data
security-audit.log
@ -205,7 +203,7 @@ auth-state-*.json
session-*.json
test-results/
test-screenshots/
# *.har
*.har
coverage/
# Allow security framework files but not sensitive data
@ -233,25 +231,19 @@ coverage/
test-actual-*.js
test-missing-*.js
direct-*.php
# *-temp.js
# *-temp.php
*-temp.js
*-temp.php
# Common ignores
.DS_Store
Thumbs.db
# *.log
# *.zip
# *.tar
# *.tar.gz
*.log
*.zip
*.tar
*.tar.gz
node_modules/
vendor/
.idea/
.vscode/
# *.swp
# *.swo
# GEMINI Config
!GEMINI.md
!.agent/
!.agent/workflows/
!.agent/workflows/*.md
*.swp
*.swo

351
Status.md
View file

@ -1,351 +0,0 @@
# HVAC Community Events - Project Status
**Last Updated:** December 16, 2025
**Current Session:** Gemini Development Transition & PHP 8+ Validation
**Version:** 2.1.7 (Staging)
---
## 🎯 CURRENT SESSION - ZOHO CRM INTEGRATION SETUP (Dec 16, 2025)
### Zoho CRM Integration - Production Issue (BLOCKING)
**Objective:** Configure and test Zoho CRM sync implementation for production environment.
**Status:** 🔴 BLOCKING - Credential save hangs on production (400 Bad Request)
### Active Issue - Credential Save Hanging on Production
**Problem:** When saving Zoho CRM credentials at `https://upskillhvac.com/wp-admin/admin.php?page=hvac-zoho-sync`, the AJAX request returns a 400 Bad Request error and the form hangs on "Saving...".
**Console Error:**
```
POST https://upskillhvac.com/wp-admin/admin-ajax.php 400 (Bad Request)
```
**Investigation Completed:**
1. ✅ Fixed Client ID regex to allow lowercase letters (`[A-Z0-9]` → `[A-Za-z0-9]`)
2. ✅ Fixed credential storage mismatch - all methods now use `HVAC_Secure_Storage`
3. ✅ Updated `HVAC_Zoho_CRM_Auth` class to use encrypted storage consistently
4. ✅ Updated OAuth callback to use secure storage
5. ✅ Updated test_connection to use secure storage
6. ✅ Deployed fixes to production - **Issue persists**
**Files Modified:**
- `includes/admin/class-zoho-admin.php` - Secure storage for credentials, fixed regex
- `includes/zoho/class-zoho-crm-auth.php` - All credential operations use HVAC_Secure_Storage
**Next Steps for Investigation:**
1. Check PHP error logs on production server for detailed error
2. Test AJAX endpoint directly via curl to isolate frontend vs backend issue
3. Verify nonce generation and validation on production
4. Check if WAF/security plugin is blocking the request
5. Test with browser network tab to see exact request/response
**Possible Causes:**
- Server-side security rules (Cloudflare, ModSecurity) blocking POST to admin-ajax.php
- Nonce validation failing due to caching
- Plugin conflict on production
- HVAC_Secure_Storage encryption key difference between environments
---
### Zoho CRM Integration - Staging Environment (Working)
**Status:** ✅ OAuth Working, Sync Methods Implemented, Dry-Run Tested
**Completed:**
1. ✅ **OAuth Authentication Verified**
- Refresh token exists and is valid
- API connection successful (53 modules accessible)
- Read operations working (Contacts, Campaigns, Users)
2. ✅ **Read-Only API Tests Passed**
- Organization Info: Manifold Cloud Services (America/Detroit)
- Contacts: 5+ records readable (Tanner Moore, Pete Knochelmann, etc.)
- Campaigns: 5+ records readable (Nov 28, Oct 23, etc.)
- CRM Users: Ben Reed (CEO), JR Lawhorne (Manager), etc.
3. ✅ **Sync Class Bug Fixes**
- Fixed user roles: `trainer`/`trainee` → `hvac_trainer`/`hvac_master_trainer`
- Fixed event filter: Removed restrictive `_hvac_event_type` meta query
- Fixed event display: Changed `eventDisplay` from `list` to `custom` to include past events
- Fixed WooCommerce dependency: Added graceful error handling
4. ✅ **Event Tickets Integration (NEW)**
- Replaced WooCommerce sync with Event Tickets (Tickets Commerce) support
- Added `sync_attendees()` method → Zoho Contacts + Campaign Members
- Added `sync_rsvps()` method → Zoho Leads + Campaign Members
- Updated meta keys for Tickets Commerce (`_tec_tickets_commerce_*`)
- Updated meta keys for RSVPs (`_tribe_rsvp_*`)
5. ✅ **Admin Interface Updated**
- Added "Sync Attendees" button (Contacts + Campaign Members)
- Added "Sync RSVPs" button (Leads + Campaign Members)
- Renamed "Sync Purchases" to "Sync Orders" (Tickets Commerce)
**Dry-Run Results (Staging - No Data Sent to Zoho):**
| Sync Type | Records Found | Status |
|-----------|---------------|--------|
| Events → Campaigns | 20 | ✅ Ready |
| Trainers → Contacts | 53 | ✅ Ready |
| Attendees → Contacts + Campaign Members | 79 | ✅ Ready |
| RSVPs → Leads + Campaign Members | 4 | ✅ Ready |
| Orders → Invoices | 52 | ✅ Ready |
**Zoho CRM Mapping Strategy:**
- **Events****Campaigns** (direct mapping)
- **Trainers** (hvac_trainer, hvac_master_trainer) → **Contacts** (with Contact_Type field)
- **Ticket Attendees****Contacts** + **Campaign Members** (links Contact ↔ Campaign)
- **RSVPs****Leads** + **Campaign Members** (links Lead ↔ Campaign)
- **Ticket Orders****Invoices** (financial records)
**Staging Protection Active:**
- All write operations (POST/PUT/DELETE) are blocked on staging
- Only production (`upskillhvac.com`) can write to Zoho CRM
- Dry-run shows what would sync without actually sending data
**Admin Page Location:**
- `/wp-admin/admin.php?page=hvac-zoho-sync`
**Files Modified:**
- `includes/zoho/class-zoho-sync.php` - Complete rewrite for Event Tickets
- `includes/admin/class-zoho-admin.php` - Added new sync buttons
---
## 📅 PREVIOUS SESSION - GEMINI TRANSITION & VALIDATION (Dec 16, 2025)
### Gemini Development Environment Setup
**Objective:** Transition from Claude Code-specific tooling to Gemini/Antigravity agent development workflow.
**Completed:**
1. ✅ **Created `GEMINI.md`** - New development guidelines
- Critical safety constraints for Cloudways Shared VPS
- Workflows for testing (`/test`) and deployment
- Coding standards (Singleton pattern, security, PHP 8+ modernization)
- Agent personas (Tester, Security Auditor, Deployment Engineer)
2. ✅ **Environment Configuration**
- Updated `.gitignore` to allow `.agent/`, `.mcp.json`, `GEMINI.md`
- Created `/home/ben/dev/upskill-event-manager/.agent/workflows/test.md`
- Fixed file access blocked by gitignore
3. ✅ **PHP 8+ Compatibility Verification**
- **Issue:** `true|\WP_Error` syntax causing PHP fatal errors on staging (PHP 8.0)
- **Fix:** Changed to `bool|\WP_Error` in `includes/class-hvac-security-helpers.php:231`
- **Status:** Deployed to staging, verified working
4. ✅ **Comprehensive Test Suite**
- **File:** `test-comprehensive-validation.js` (Playwright E2E tests)
- **Fixed:** Login form selectors (`#user_login`, `#user_pass`, `#wp-submit`)
- **Modes:** Headless (default) or headed (`DISPLAY=:1 HEADLESS=false`)
- **Results:**
- Master Trainer pages: ✅ ALL PASSING (4/4)
- Security endpoints: ✅ ALL SECURE (4/4)
- Trainer pages: ⚠️ Require authentication (expected)
**Test Results Summary:**
```
✅ Master Dashboard - Functional with navigation
✅ Announcements - Fully functional & responsive
✅ Pending Approvals - Fully functional & responsive
✅ Trainers - Fully functional & responsive
🔒 Security: All AJAX endpoints properly secured (401/400 responses)
- hvac_get_trainer_stats
- hvac_manage_announcement
- hvac_approve_trainer
- hvac_approve_trainer_v2
```
**Test Credentials Updated:**
- `test_master` / `Test123!` (hvac_master_trainer)
- `test_trainer` / `Test123!` (hvac_trainer)
- `test_admin` / `Test123!` (administrator)
5. ✅ **Master Trainer Navigation Dropdown Fix** (Dec 16, 2025)
- **Issue:** Green/teal colored boxes appearing in navigation toolbar instead of dropdown arrows
- **Root Cause:** Empty `<span class="menu-toggle">` elements with CSS background styling
- **Fix:** Replaced with `<span class="dropdown-arrow">▼</span>` in `includes/class-hvac-master-menu-system.php:327`
- **Impact:** All master trainer pages (`/master-trainer/*`)
- **Status:** ✅ Deployed to staging, verified working
- **Verification:** Screenshots confirm dropdown arrows display correctly, green boxes removed
---
## 📁 RECENT DEPLOYMENTS
### v2.1.7 - Critical Nonce Fix (Nov 3, 2025)
**Issue:** Announcement submission completely broken - nonce mismatch
**Fix:** Changed nonce action from `hvac_announcements_admin_nonce``hvac_announcements_nonce`
**Files:** `includes/class-hvac-announcements-admin.php` (line 96)
**Status:** ✅ Deployed to staging, fully functional
### v2.1.6 - Technical Debt Cleanup
**Fixes:**
1. Version synchronization (2.0.0 → 2.1.6 in plugin header)
2. FOUC prevention (modal `display: none` by default)
3. Conditional logging (`error_log()` → `HVAC_Logger::log()`)
### v2.1.5 - Z-Index Stacking Fix
**Issue:** WordPress media modal appearing behind announcement modal
**Fix:** Reduced announcement modal z-index from 999999 → 100000
**Result:** Media modals (z-index 160000) now properly stack on top
---
## 🧪 TESTING INFRASTRUCTURE
### Comprehensive Test Suite
**File:** `test-comprehensive-validation.js`
**Framework:** Playwright (Node.js)
**Run Tests:**
```bash
# Headless (default)
node test-comprehensive-validation.js
# Headed mode (visible browser)
DISPLAY=:1 HEADLESS=false node test-comprehensive-validation.js
```
**Test Coverage:**
- ✅ Trainer pages (4 pages)
- ✅ Master trainer pages (4 pages)
- ✅ Security/AJAX endpoints (4 endpoints)
- ✅ Layout & responsive design
- ✅ Authentication flows
---
## 🚀 DEPLOYMENT
### Staging Environment
**URL:** https://upskill-staging.measurequick.com
**Version:** 2.1.7 + PHP 8+ fixes
**Server:** Cloudways Shared VPS (PHP 8.0)
**Status:** ✅ Fully functional
**Deploy to Staging:**
```bash
./scripts/deploy.sh staging
```
**Verify Deployment:**
```bash
./scripts/verify-plugin-fixes.sh
```
### Production Environment
**URL:** https://upskillhvac.com
**Version:** 2.1.7 (pending deployment)
**Server:** Cloudways Shared VPS
---
## 🔧 KEY DEVELOPMENT GUIDELINES
### GEMINI.md Rules (NEW)
1. **Safety First:**
- NEVER delete files outside project directory
- NEVER execute `rm -rf` without confirmation
- NEVER modify system configs (`/etc/*`, `/var/*`)
- NEVER deploy to production without explicit request
2. **Infrastructure Constraints:**
- Cloudways Shared VPS (limited resources)
- Do NOT force PHP version changes
- Do NOT install system-level packages
- Be mindful of CPU/RAM usage
3. **Testing Mandatory:**
```bash
node test-comprehensive-validation.js
```
4. **Security Standards:**
- Always sanitize input
- Always escape output
- Verify nonces on forms & AJAX
- Check roles/capabilities
### WordPress Architecture
- **Singleton Pattern:** All core classes use `::instance()`
- **Template Security:** All templates start with security check
- **PHP 8+ Modernization:** In progress (avoid PHP 8.2+ features)
---
## 📚 DOCUMENTATION
### Primary Files
- **`GEMINI.md`** - Gemini agent development guidelines (NEW)
- **`CLAUDE.md`** - Claude Code agent guidelines (legacy)
- **`docs/ARCHITECTURE.md`** - Plugin architecture details
- **`docs/CLAUDE-CODE-DEVELOPMENT-BEST-PRACTICES.md`** - Development patterns
### Workflows
- **`.agent/workflows/test.md`** - Running comprehensive tests (`/test`)
---
## 📋 NEXT ACTIONS
### Immediate
1. ⏳ **Production Deployment** - Deploy v2.1.7 + PHP 8+ fixes (pending user approval)
2. ✅ **PHP 8+ Modernization** - Continue Phase 2 modernization
3. 🔜 **Enhancements** - New features for next session
### Pre-Production Checklist
- ✅ PHP 8+ compatibility verified
- ✅ Security endpoints validated
- ✅ Master trainer pages functional
- ✅ Comprehensive tests passing
- ✅ No fatal errors on staging
**Deploy Command:**
```bash
./scripts/deploy.sh production
```
---
## ⚠️ KNOWN ISSUES
### Minor (Non-Blocking)
1. **Playwright Headless Login** - Works in headed mode with correct selectors
2. **jQuery Loading Timing** - Brief "jQuery is not defined" error (non-blocking)
3. **Dashboard Responsive** - Minor responsive layout issue (cosmetic)
---
## 📊 SUMMARY
**Current State:** ✅ **PRODUCTION READY**
**Key Achievements:**
- Gemini development environment established
- PHP 8+ compatibility verified and deployed
- Comprehensive test suite functional (headed mode)
- All security endpoints properly secured
- Master trainer features fully operational
- Test accounts updated and working
**Quality Metrics:**
- Test Coverage: 8 pages + 4 security endpoints
- Success Rate: 100% master trainer pages
- Security: 100% endpoints secured
- PHP Compatibility: ✅ No fatal errors
**Agent Transition:**
- From: Claude Code + MCP tools
- To: Gemini/Antigravity + direct tooling
- Status: ✅ Complete and validated
---
*For detailed historical context, see git history and previous Status.md versions*

View file

@ -1,168 +0,0 @@
/**
* Zoho CRM Admin Styles
*
* @package HVACCommunityEvents
*/
/* Card Layout */
.hvac-zoho-wrap {
max-width: 1200px;
}
.hvac-zoho-wrap .card {
max-width: 100%;
padding: 20px;
margin-bottom: 20px;
}
.hvac-zoho-wrap h2 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #ccd0d4;
}
/* Form Styles */
.hvac-zoho-wrap table.form-table th {
width: 200px;
}
.hvac-zoho-wrap .regular-text {
width: 400px;
}
.hvac-zoho-wrap code {
background: #f0f0f1;
padding: 4px 8px;
border-radius: 3px;
font-size: 13px;
}
/* Button Groups */
.hvac-zoho-wrap .button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 15px;
}
/* Sync Buttons */
.hvac-zoho-wrap .sync-button {
min-width: 150px;
}
/* Status Messages */
.hvac-zoho-wrap .sync-status {
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-left: 4px solid #007cba;
}
.hvac-zoho-wrap .sync-status.success {
border-left-color: #00a32a;
}
.hvac-zoho-wrap .sync-status.error {
border-left-color: #d63638;
}
/* Connection Status */
#connection-status {
margin-top: 15px;
}
#connection-status .notice {
margin: 0;
}
/* Staging Mode Banner */
.hvac-zoho-wrap .staging-notice {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.hvac-zoho-wrap .staging-notice h3 {
margin: 0 0 10px 0;
color: #856404;
}
/* Debug Info */
.hvac-zoho-debug-info {
margin-top: 15px;
padding: 10px;
background: #f0f0f1;
border-radius: 4px;
font-size: 12px;
}
.hvac-zoho-debug-info details {
margin-top: 10px;
}
.hvac-zoho-debug-info summary {
cursor: pointer;
color: #007cba;
}
.hvac-zoho-debug-info pre {
margin: 10px 0 0 0;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Credentials Form */
#zoho-credentials-form .description {
color: #646970;
font-style: italic;
margin-top: 5px;
}
/* Password Toggle */
#toggle-secret {
margin-left: 10px;
vertical-align: middle;
}
/* Sync Results */
.sync-results {
margin-top: 15px;
}
.sync-results ul {
margin: 10px 0;
padding-left: 20px;
}
.sync-results details {
margin-top: 10px;
}
.sync-results pre {
background: #f0f0f1;
padding: 10px;
overflow: auto;
max-height: 300px;
font-size: 12px;
}
/* Responsive */
@media screen and (max-width: 782px) {
.hvac-zoho-wrap table.form-table th {
width: auto;
}
.hvac-zoho-wrap .regular-text {
width: 100%;
}
.hvac-zoho-wrap .button-group {
flex-direction: column;
}
.hvac-zoho-wrap .sync-button {
width: 100%;
}
}

View file

@ -1,131 +1,8 @@
/**
* Zoho CRM Admin JavaScript
*
* @package HVACCommunityEvents
*/
jQuery(document).ready(function($) {
// =====================================================
// Password visibility toggle
// =====================================================
$('#toggle-secret').on('click', function() {
var passwordField = $('#zoho_client_secret');
var toggleBtn = $(this);
if (passwordField.attr('type') === 'password') {
passwordField.attr('type', 'text');
toggleBtn.text('Hide');
} else {
passwordField.attr('type', 'password');
toggleBtn.text('Show');
}
});
// =====================================================
// Copy redirect URI to clipboard
// =====================================================
$('#copy-redirect-uri').on('click', function() {
var redirectUri = hvacZoho.redirectUri || '';
if (!redirectUri) {
alert('Redirect URI not available');
return;
}
navigator.clipboard.writeText(redirectUri).then(function() {
$('#copy-redirect-uri').text('Copied!').prop('disabled', true);
setTimeout(function() {
$('#copy-redirect-uri').text('Copy').prop('disabled', false);
}, 2000);
}).catch(function() {
// Fallback for older browsers
var tempInput = $('<input>');
$('body').append(tempInput);
tempInput.val(redirectUri).select();
document.execCommand('copy');
tempInput.remove();
$('#copy-redirect-uri').text('Copied!').prop('disabled', true);
setTimeout(function() {
$('#copy-redirect-uri').text('Copy').prop('disabled', false);
}, 2000);
});
});
// =====================================================
// Flush rewrite rules
// =====================================================
$('#flush-rewrite-rules').on('click', function() {
var button = $(this);
button.prop('disabled', true).text('Flushing...');
$.post(hvacZoho.ajaxUrl, {
action: 'hvac_zoho_flush_rewrite_rules'
}, function(response) {
if (response.success) {
button.text('Flushed!').css('color', '#46b450');
setTimeout(function() {
location.reload();
}, 1000);
} else {
button.text('Error').css('color', '#dc3232');
setTimeout(function() {
button.prop('disabled', false).text('Flush Rules').css('color', '');
}, 2000);
}
});
});
// =====================================================
// Credentials form submission
// =====================================================
$('#zoho-credentials-form').on('submit', function(e) {
e.preventDefault();
var formData = {
action: 'hvac_zoho_save_credentials',
zoho_client_id: $('#zoho_client_id').val(),
zoho_client_secret: $('#zoho_client_secret').val(),
nonce: $('input[name="hvac_zoho_nonce"]').val()
};
$('#save-credentials').prop('disabled', true).text('Saving...');
$.post(hvacZoho.ajaxUrl, formData, function(response) {
if (response.success) {
window.location.href = window.location.href.split('?')[0] + '?page=hvac-zoho-sync&credentials_saved=1';
} else {
alert('Error saving credentials: ' + response.data.message);
$('#save-credentials').prop('disabled', false).text('Save Credentials');
}
});
});
// =====================================================
// OAuth authorization handler
// =====================================================
$('#start-oauth').on('click', function() {
var clientId = $('#zoho_client_id').val();
var clientSecret = $('#zoho_client_secret').val();
if (!clientId || !clientSecret) {
alert('Please save your credentials first before starting OAuth authorization.');
return;
}
// Use server-generated OAuth URL with CSRF state parameter
var oauthUrl = hvacZoho.oauthUrl || '';
if (!oauthUrl) {
alert('OAuth URL not available. Please save your credentials first and refresh the page.');
return;
}
// Open OAuth URL in the same window to handle callback properly
window.location.href = oauthUrl;
});
// =====================================================
// Test connection
// =====================================================
$('#test-connection').on('click', function() {
var $button = $(this);
var $status = $('#connection-status');

View file

@ -43,8 +43,6 @@ class HVAC_Zoho_Admin {
add_action('wp_ajax_hvac_zoho_sync_data', array($this, 'sync_data'));
add_action('wp_ajax_hvac_zoho_save_credentials', array($this, 'save_credentials'));
add_action('wp_ajax_hvac_zoho_flush_rewrite_rules', array($this, 'flush_rewrite_rules_ajax'));
add_action('wp_ajax_hvac_zoho_save_settings', array($this, 'save_settings'));
add_action('wp_ajax_hvac_zoho_run_scheduled_sync', array($this, 'run_scheduled_sync_now'));
// Add simple test handler
add_action('wp_ajax_hvac_zoho_simple_test', array($this, 'simple_test'));
// Add OAuth callback handler - only use one method to prevent duplicates
@ -52,22 +50,8 @@ class HVAC_Zoho_Admin {
add_filter('query_vars', array($this, 'add_oauth_query_vars'), 10, 1);
add_action('template_redirect', array($this, 'handle_oauth_template_redirect'));
// Fallback: Check for OAuth params on init (in case rewrite rules fail and we land on homepage)
add_action('init', array($this, 'check_for_oauth_params'));
// Ensure rewrite rules are flushed when plugin is activated
register_activation_hook(HVAC_PLUGIN_FILE, array($this, 'flush_rewrite_rules_on_activation'));
// Initialize scheduled sync if enabled
$this->init_scheduled_sync();
}
/**
* Initialize scheduled sync
*/
private function init_scheduled_sync() {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
HVAC_Zoho_Scheduled_Sync::instance();
}
/**
@ -93,23 +77,6 @@ class HVAC_Zoho_Admin {
return;
}
$site_url = get_site_url();
$redirect_uri = $site_url . '/oauth/callback';
// Get OAuth URL if credentials exist
$oauth_url = '';
if (!class_exists('HVAC_Secure_Storage')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-secure-storage.php';
}
$client_id = HVAC_Secure_Storage::get_credential('hvac_zoho_client_id', '');
$client_secret = HVAC_Secure_Storage::get_credential('hvac_zoho_client_secret', '');
if (!empty($client_id) && !empty($client_secret)) {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-crm-auth.php';
$auth = new HVAC_Zoho_CRM_Auth();
$oauth_url = $auth->get_authorization_url();
}
wp_enqueue_script(
'hvac-zoho-admin',
HVAC_PLUGIN_URL . 'assets/js/zoho-admin.js',
@ -120,11 +87,22 @@ class HVAC_Zoho_Admin {
wp_localize_script('hvac-zoho-admin', 'hvacZoho', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_zoho_nonce'),
'redirectUri' => $redirect_uri,
'oauthUrl' => $oauth_url
'nonce' => wp_create_nonce('hvac_zoho_nonce')
));
// Add inline script for debugging (only in development)
if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
wp_add_inline_script('hvac-zoho-admin', '
console.log("Zoho admin script loaded");
jQuery(document).ready(function($) {
console.log("DOM ready, setting up click handler");
$(document).on("click", "#test-zoho-connection", function() {
console.log("Test button clicked - inline script");
});
});
');
}
wp_enqueue_style(
'hvac-zoho-admin',
HVAC_PLUGIN_URL . 'assets/css/zoho-admin.css',
@ -141,13 +119,27 @@ class HVAC_Zoho_Admin {
// Debug logging
// Ensure Auth class is loaded for staging detection
if (!class_exists('HVAC_Zoho_CRM_Auth')) {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-crm-auth.php';
// More robust production detection
$parsed_url = parse_url($site_url);
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
// Remove www prefix for comparison
$clean_host = preg_replace('/^www\./', '', $host);
// Check if this is production
$is_production = ($clean_host === 'upskillhvac.com');
// Double-check with string comparison as fallback
if (!$is_production) {
$is_production = (strpos($site_url, 'upskillhvac.com') !== false &&
strpos($site_url, 'staging') === false &&
strpos($site_url, 'test') === false &&
strpos($site_url, 'dev') === false &&
strpos($site_url, 'cloudwaysapps.com') === false);
}
// Use central logic for staging detection
$is_staging = HVAC_Zoho_CRM_Auth::is_staging_mode();
// Set staging as opposite of production
$is_staging = !$is_production;
// Load secure storage class
@ -160,7 +152,6 @@ class HVAC_Zoho_Admin {
$client_secret = HVAC_Secure_Storage::get_credential('hvac_zoho_client_secret', '');
$stored_refresh_token = HVAC_Secure_Storage::get_credential('hvac_zoho_refresh_token', '');
$has_credentials = !empty($client_id) && !empty($client_secret);
// OAuth URL is generated in enqueue_admin_scripts() and passed via wp_localize_script()
// Handle form submission
if (isset($_GET['credentials_saved'])) {
@ -270,9 +261,6 @@ class HVAC_Zoho_Admin {
🚀 Authorize with Zoho
</button>
<?php endif; ?>
<button type="button" class="button button-secondary" id="diagnostic-test" style="margin-left: 10px;">
🏥 Run Diagnostic Test
</button>
</p>
</form>
</div>
@ -305,140 +293,150 @@ class HVAC_Zoho_Admin {
</div>
<div class="sync-section">
<h3>Trainers Contacts</h3>
<p>Sync trainers (hvac_trainer, hvac_master_trainer) to Zoho CRM Contacts</p>
<button class="button sync-button" data-type="users">Sync Trainers</button>
<h3>Users Contacts</h3>
<p>Sync trainers and attendees to Zoho CRM Contacts</p>
<button class="button sync-button" data-type="users">Sync Users</button>
<div class="sync-status" id="users-status"></div>
</div>
<div class="sync-section">
<h3>Ticket Attendees Contacts + Campaign Members</h3>
<p>Sync Event Tickets attendees to Zoho CRM Contacts and link them to Campaigns</p>
<button class="button sync-button" data-type="attendees">Sync Attendees</button>
<div class="sync-status" id="attendees-status"></div>
</div>
<div class="sync-section">
<h3>RSVPs Leads + Campaign Members</h3>
<p>Sync RSVP responses to Zoho CRM Leads and link them to Campaigns</p>
<button class="button sync-button" data-type="rsvps">Sync RSVPs</button>
<div class="sync-status" id="rsvps-status"></div>
</div>
<div class="sync-section">
<h3>Ticket Orders Invoices</h3>
<p>Sync Event Tickets orders (Tickets Commerce) to Zoho CRM Invoices</p>
<button class="button sync-button" data-type="purchases">Sync Orders</button>
<h3>Purchases Invoices</h3>
<p>Sync ticket purchases to Zoho CRM Invoices</p>
<button class="button sync-button" data-type="purchases">Sync Purchases</button>
<div class="sync-status" id="purchases-status"></div>
</div>
</div>
<div class="hvac-zoho-settings">
<h2>Scheduled Sync Settings</h2>
<?php
// Get scheduled sync status
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
$sync_status = $scheduled_sync->get_status();
$current_frequency = get_option('hvac_zoho_sync_frequency', 'every_5_minutes');
?>
<h2>Sync Settings</h2>
<form id="zoho-settings-form">
<table class="form-table">
<tr>
<th scope="row">Enable Scheduled Sync</th>
<td>
<label>
<input type="checkbox" name="auto_sync" value="1" <?php checked(get_option('hvac_zoho_auto_sync'), '1'); ?>>
Automatically sync new/modified records to Zoho CRM
</label>
<p class="description">When enabled, a background process will sync changes on the selected interval.</p>
</td>
</tr>
<tr>
<th scope="row">Sync Interval</th>
<td>
<select name="sync_frequency">
<option value="every_5_minutes" <?php selected($current_frequency, 'every_5_minutes'); ?>>Every 5 minutes</option>
<option value="every_15_minutes" <?php selected($current_frequency, 'every_15_minutes'); ?>>Every 15 minutes</option>
<option value="every_30_minutes" <?php selected($current_frequency, 'every_30_minutes'); ?>>Every 30 minutes</option>
<option value="hourly" <?php selected($current_frequency, 'hourly'); ?>>Hourly</option>
<option value="every_6_hours" <?php selected($current_frequency, 'every_6_hours'); ?>>Every 6 hours</option>
<option value="daily" <?php selected($current_frequency, 'daily'); ?>>Daily</option>
</select>
<p class="description">How often to check for and sync new/modified records.</p>
</td>
</tr>
<tr>
<th scope="row">Sync Status</th>
<td>
<p>
<strong>Status:</strong>
<?php if ($sync_status['is_scheduled']): ?>
<span style="color: #46b450;"> Scheduled</span>
<?php else: ?>
<span style="color: #dc3232;"> Not Scheduled</span>
<?php endif; ?>
</p>
<p>
<strong>Last Sync:</strong>
<?php echo esc_html($sync_status['last_sync_formatted']); ?>
</p>
<?php if ($sync_status['is_scheduled']): ?>
<p>
<strong>Next Sync:</strong>
<?php echo esc_html($sync_status['next_sync_formatted']); ?>
</p>
<?php endif; ?>
<?php if ($sync_status['last_result']): ?>
<p>
<strong>Last Result:</strong>
<?php
$last = $sync_status['last_result'];
echo esc_html(sprintf('%d synced, %d failed', $last['total_synced'] ?? 0, $last['total_failed'] ?? 0));
?>
</p>
<?php endif; ?>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary">Save Settings</button>
<button type="button" class="button button-secondary" id="run-scheduled-sync-now" style="margin-left: 10px;">
🔄 Run Sync Now
</button>
</p>
<label>
<input type="checkbox" name="auto_sync" value="1" <?php checked(get_option('hvac_zoho_auto_sync'), '1'); ?>>
Enable automatic sync
</label>
<br><br>
<label>
Sync frequency:
<select name="sync_frequency">
<option value="hourly" <?php selected(get_option('hvac_zoho_sync_frequency'), 'hourly'); ?>>Hourly</option>
<option value="daily" <?php selected(get_option('hvac_zoho_sync_frequency'), 'daily'); ?>>Daily</option>
<option value="weekly" <?php selected(get_option('hvac_zoho_sync_frequency'), 'weekly'); ?>>Weekly</option>
</select>
</label>
<br><br>
<button type="submit" class="button button-primary">Save Settings</button>
</form>
<div id="scheduled-sync-status"></div>
</div>
<?php endif; ?>
</div>
<script>
jQuery(document).ready(function($) {
// Toggle password visibility
$('#toggle-secret').on('click', function() {
var passwordField = $('#zoho_client_secret');
var toggleBtn = $(this);
if (passwordField.attr('type') === 'password') {
passwordField.attr('type', 'text');
toggleBtn.text('Hide');
} else {
passwordField.attr('type', 'password');
toggleBtn.text('Show');
}
});
// Copy redirect URI to clipboard
$('#copy-redirect-uri').on('click', function() {
var redirectUri = '<?php echo esc_js($site_url . '/oauth/callback'); ?>';
navigator.clipboard.writeText(redirectUri).then(function() {
$('#copy-redirect-uri').text('Copied!').prop('disabled', true);
setTimeout(function() {
$('#copy-redirect-uri').text('Copy').prop('disabled', false);
}, 2000);
});
});
// Flush rewrite rules
$('#flush-rewrite-rules').on('click', function() {
var button = $(this);
button.prop('disabled', true).text('Flushing...');
$.post(ajaxurl, {
action: 'hvac_zoho_flush_rewrite_rules'
}, function(response) {
if (response.success) {
button.text('Flushed!').css('color', '#46b450');
setTimeout(function() {
location.reload(); // Reload to update the status
}, 1000);
} else {
button.text('Error').css('color', '#dc3232');
setTimeout(function() {
button.prop('disabled', false).text('Flush Rules').css('color', '');
}, 2000);
}
});
});
// Handle credentials form submission
$('#zoho-credentials-form').on('submit', function(e) {
e.preventDefault();
var formData = {
action: 'hvac_zoho_save_credentials',
zoho_client_id: $('#zoho_client_id').val(),
zoho_client_secret: $('#zoho_client_secret').val(),
nonce: $('input[name="hvac_zoho_nonce"]').val()
};
$('#save-credentials').prop('disabled', true).text('Saving...');
$.post(ajaxurl, formData, function(response) {
if (response.success) {
window.location.href = window.location.href + '&credentials_saved=1';
} else {
alert('Error saving credentials: ' + response.data.message);
$('#save-credentials').prop('disabled', false).text('Save Credentials');
}
});
});
// Handle OAuth authorization
$('#start-oauth').on('click', function() {
var clientId = $('#zoho_client_id').val();
var clientSecret = $('#zoho_client_secret').val();
if (!clientId || !clientSecret) {
alert('Please save your credentials first before starting OAuth authorization.');
return;
}
// Generate OAuth URL
var redirectUri = '<?php echo esc_js($site_url . '/oauth/callback'); ?>';
var scopes = 'ZohoCRM.settings.ALL,ZohoCRM.modules.ALL,ZohoCRM.users.ALL,ZohoCRM.org.ALL,ZohoCRM.bulk.READ';
var oauthUrl = 'https://accounts.zoho.com/oauth/v2/auth?' +
'scope=' + encodeURIComponent(scopes) +
'&client_id=' + encodeURIComponent(clientId) +
'&response_type=code' +
'&access_type=offline' +
'&redirect_uri=' + encodeURIComponent(redirectUri) +
'&prompt=consent';
// Open OAuth URL in the same window to handle callback properly
window.location.href = oauthUrl;
});
});
</script>
<?php
// Note: All JavaScript functionality moved to assets/js/zoho-admin.js
// Data is passed via wp_localize_script() in enqueue_admin_scripts()
}
/**
* Simple test handler to isolate issues
*/
public function simple_test() {
// Check permissions
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Unauthorized access'));
return;
}
$payload_status = 'No payload received';
if (!empty($_POST['test_payload'])) {
$payload_status = 'Payload received (' . strlen($_POST['test_payload']) . ' chars)';
}
wp_send_json_success(array(
'message' => 'Simple test works!',
'server_time' => date('Y-m-d H:i:s'),
'payload_status' => $payload_status,
'request_method' => $_SERVER['REQUEST_METHOD']
));
wp_send_json_success(array('message' => 'Simple test works!'));
}
/**
@ -464,7 +462,7 @@ class HVAC_Zoho_Admin {
}
// Validate Client ID format (should start with "1000.")
if (!preg_match('/^1000\.[A-Za-z0-9]+$/', $client_id)) {
if (!preg_match('/^1000\.[A-Z0-9]+$/', $client_id)) {
wp_send_json_error(array('message' => 'Invalid Client ID format. Should start with "1000."'));
return;
}
@ -566,6 +564,9 @@ class HVAC_Zoho_Admin {
}
}
/**
* Parse OAuth request using parse_request hook
*/
public function parse_oauth_request($wp) {
// Check if this is an OAuth callback request
@ -582,33 +583,6 @@ class HVAC_Zoho_Admin {
}
}
/**
* Manual Router for OAuth Callback
*
* Catches the request on 'init' before WordPress internal routing can 404 it.
* This is a robust fallback for when rewrite rules fail or haven't flushed.
*/
public function check_for_oauth_params() {
// Check if we are at the oauth callback URL path
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Check strict path match or if regex matches
if (strpos($path, '/oauth/callback') !== false) {
// We are at the right URL. Do we have the code?
if (isset($_GET['code'])) {
// We have a code.
// We MUST process this, otherwise WP will display a 404.
// Even if state matches or not, we should handle it here.
// The process_oauth_callback method handles validation.
$this->process_oauth_callback();
// process_oauth_callback exits, so we won't continue to 404.
}
}
}
/**
* Add OAuth callback rewrite rule
*/
@ -650,33 +624,16 @@ class HVAC_Zoho_Admin {
* Process OAuth callback from Zoho
*/
public function process_oauth_callback() {
if (!isset($_GET['code'])) {
wp_die('OAuth callback missing authorization code');
}
// Validate state parameter for CSRF protection
if (!isset($_GET['state'])) {
wp_die('OAuth callback missing state parameter. Possible CSRF attack.');
}
// Load secure storage for credential handling
if (!class_exists('HVAC_Secure_Storage')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-secure-storage.php';
}
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-crm-auth.php';
$auth = new HVAC_Zoho_CRM_Auth();
if (!$auth->validate_oauth_state(sanitize_text_field($_GET['state']))) {
wp_die('OAuth state validation failed. Please try the authorization again.');
}
// Get credentials using secure storage (credentials are stored encrypted)
if (!class_exists('HVAC_Secure_Storage')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-secure-storage.php';
}
$client_id = HVAC_Secure_Storage::get_credential('hvac_zoho_client_id', '');
$client_secret = HVAC_Secure_Storage::get_credential('hvac_zoho_client_secret', '');
// Get credentials from WordPress options
$client_id = get_option('hvac_zoho_client_id', '');
$client_secret = get_option('hvac_zoho_client_secret', '');
if (empty($client_id) || empty($client_secret)) {
wp_die('OAuth callback error: Missing client credentials. Please configure your Zoho CRM credentials first.');
@ -719,19 +676,20 @@ class HVAC_Zoho_Admin {
exit;
}
// Save tokens using secure storage
HVAC_Secure_Storage::store_credential('hvac_zoho_access_token', $token_data['access_token']);
// Save tokens
update_option('hvac_zoho_access_token', $token_data['access_token']);
update_option('hvac_zoho_token_expires', time() + ($token_data['expires_in'] ?? 3600));
// Refresh token might not be returned on subsequent authorizations
if (isset($token_data['refresh_token']) && !empty($token_data['refresh_token'])) {
HVAC_Secure_Storage::store_credential('hvac_zoho_refresh_token', $token_data['refresh_token']);
update_option('hvac_zoho_refresh_token', $token_data['refresh_token']);
} else {
$existing_refresh = HVAC_Secure_Storage::get_credential('hvac_zoho_refresh_token', '');
$existing_refresh = get_option('hvac_zoho_refresh_token');
if (empty($existing_refresh)) {
// This is critical - we need a refresh token for long-term access
// Store a warning but still complete the flow
update_option('hvac_zoho_missing_refresh_token', true);
} else {
}
}
@ -762,12 +720,9 @@ class HVAC_Zoho_Admin {
return;
}
// Get credentials using secure storage (credentials are stored encrypted)
if (!class_exists('HVAC_Secure_Storage')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-secure-storage.php';
}
$client_id = HVAC_Secure_Storage::get_credential('hvac_zoho_client_id', '');
$client_secret = HVAC_Secure_Storage::get_credential('hvac_zoho_client_secret', '');
// Get credentials from WordPress options
$client_id = get_option('hvac_zoho_client_id', '');
$client_secret = get_option('hvac_zoho_client_secret', '');
// Check configuration before attempting connection
if (empty($client_id)) {
@ -791,7 +746,7 @@ class HVAC_Zoho_Admin {
}
// Check if we have stored refresh token from previous OAuth
$stored_refresh_token = HVAC_Secure_Storage::get_credential('hvac_zoho_refresh_token', '');
$stored_refresh_token = get_option('hvac_zoho_refresh_token');
if (empty($stored_refresh_token)) {
@ -896,14 +851,10 @@ class HVAC_Zoho_Admin {
return;
}
// Success!
// Success!
wp_send_json_success(array(
'message' => 'Connection successful!',
'modules' => isset($response['modules']) ? count($response['modules']) . ' modules available' : 'API connected',
'client_id' => substr($client_id, 0, 10) . '...',
'client_secret_exists' => true,
'refresh_token_exists' => true,
'credentials_status' => array(
'client_id' => substr($client_id, 0, 10) . '...',
'client_secret_exists' => true,
@ -912,33 +863,23 @@ class HVAC_Zoho_Admin {
)
));
} catch (Exception $e) {
$error_response = array(
wp_send_json_error(array(
'message' => 'Connection test failed due to exception',
'error' => $e->getMessage(),
);
// Only expose file paths in debug mode
if (defined('WP_DEBUG') && WP_DEBUG) {
$error_response['file'] = $e->getFile() . ':' . $e->getLine();
}
wp_send_json_error($error_response);
'file' => $e->getFile() . ':' . $e->getLine()
));
} catch (Error $e) {
$error_response = array(
wp_send_json_error(array(
'message' => 'Connection test failed due to PHP error',
'error' => $e->getMessage(),
);
if (defined('WP_DEBUG') && WP_DEBUG) {
$error_response['file'] = $e->getFile() . ':' . $e->getLine();
}
wp_send_json_error($error_response);
'file' => $e->getFile() . ':' . $e->getLine()
));
} catch (Throwable $e) {
$error_response = array(
wp_send_json_error(array(
'message' => 'Connection test failed due to fatal error',
'error' => $e->getMessage(),
);
if (defined('WP_DEBUG') && WP_DEBUG) {
$error_response['file'] = $e->getFile() . ':' . $e->getLine();
}
wp_send_json_error($error_response);
'file' => $e->getFile() . ':' . $e->getLine()
));
}
}
@ -953,8 +894,6 @@ class HVAC_Zoho_Admin {
}
$type = sanitize_text_field($_POST['type']);
$offset = isset($_POST['offset']) ? absint($_POST['offset']) : 0;
$limit = isset($_POST['limit']) ? absint($_POST['limit']) : 50;
try {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-sync.php';
@ -962,19 +901,13 @@ class HVAC_Zoho_Admin {
switch ($type) {
case 'events':
$result = $sync->sync_events($offset, $limit);
$result = $sync->sync_events();
break;
case 'users':
$result = $sync->sync_users($offset, $limit);
break;
case 'attendees':
$result = $sync->sync_attendees($offset, $limit);
break;
case 'rsvps':
$result = $sync->sync_rsvps($offset, $limit);
$result = $sync->sync_users();
break;
case 'purchases':
$result = $sync->sync_purchases($offset, $limit);
$result = $sync->sync_purchases();
break;
default:
throw new Exception('Invalid sync type');
@ -988,89 +921,5 @@ class HVAC_Zoho_Admin {
));
}
}
/**
* Save scheduled sync settings
*/
public function save_settings() {
check_ajax_referer('hvac_zoho_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Unauthorized access'));
return;
}
$auto_sync = isset($_POST['auto_sync']) && $_POST['auto_sync'] === '1' ? '1' : '0';
$sync_frequency = sanitize_text_field($_POST['sync_frequency'] ?? 'every_5_minutes');
// Validate frequency value
$valid_frequencies = array(
'every_5_minutes',
'every_15_minutes',
'every_30_minutes',
'hourly',
'every_6_hours',
'daily'
);
if (!in_array($sync_frequency, $valid_frequencies)) {
$sync_frequency = 'every_5_minutes';
}
// Save settings
update_option('hvac_zoho_auto_sync', $auto_sync);
update_option('hvac_zoho_sync_frequency', $sync_frequency);
// Get scheduled sync instance and explicitly schedule/unschedule
// This ensures scheduling works even if option value didn't change
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
if ($auto_sync === '1') {
$scheduled_sync->schedule_sync($sync_frequency);
} else {
$scheduled_sync->unschedule_sync();
}
$status = $scheduled_sync->get_status();
wp_send_json_success(array(
'message' => 'Settings saved successfully',
'auto_sync' => $auto_sync,
'sync_frequency' => $sync_frequency,
'is_scheduled' => $status['is_scheduled'],
'next_sync' => $status['next_sync_formatted']
));
}
/**
* Run scheduled sync manually
*/
public function run_scheduled_sync_now() {
check_ajax_referer('hvac_zoho_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Unauthorized access'));
return;
}
try {
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-scheduled-sync.php';
$scheduled_sync = HVAC_Zoho_Scheduled_Sync::instance();
$result = $scheduled_sync->run_now();
wp_send_json_success(array(
'message' => 'Scheduled sync completed',
'result' => $result
));
} catch (Exception $e) {
wp_send_json_error(array(
'message' => 'Sync failed',
'error' => $e->getMessage()
));
}
}
}
?>

View file

@ -66,8 +66,7 @@ class HVAC_Master_Content_Injector {
*/
private function __construct() {
add_filter('the_content', array($this, 'inject_master_content'), 10);
// Use wp_footer instead of wp_head to ensure jQuery is loaded first
add_action('wp_footer', array($this, 'inject_inline_content'), 20);
add_action('wp_head', array($this, 'inject_inline_content'), 1);
}
/**
@ -221,19 +220,16 @@ class HVAC_Master_Content_Injector {
return '';
}
// Try direct method call first - check which method exists
// Try direct method call first
if (class_exists($shortcode_data['class'])) {
$instance = null;
if (method_exists($shortcode_data['class'], 'get_instance')) {
$instance = call_user_func(array($shortcode_data['class'], 'instance'));
if (!$instance && method_exists($shortcode_data['class'], 'get_instance')) {
$instance = call_user_func(array($shortcode_data['class'], 'get_instance'));
} elseif (method_exists($shortcode_data['class'], 'instance')) {
$instance = call_user_func(array($shortcode_data['class'], 'instance'));
}
if ($instance && method_exists($instance, $shortcode_data['method'])) {
ob_start();
// Pass empty array to satisfy shortcode callback signature
echo $instance->{$shortcode_data['method']}(array());
echo $instance->{$shortcode_data['method']}();
return ob_get_clean();
}
}

View file

@ -327,7 +327,7 @@ class HVAC_Master_Menu_System {
echo '<span class="menu-title">' . esc_html($item['title']) . '</span>';
if ($has_children) {
echo '<span class="dropdown-arrow">▼</span>';
echo '<span class="menu-toggle" aria-hidden="true"></span>';
}
echo '</a>';

View file

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

View file

@ -23,15 +23,10 @@ class HVAC_Zoho_CRM_Auth {
private $last_error = null;
public function __construct() {
// Load secure storage class
if (!class_exists('HVAC_Secure_Storage')) {
require_once plugin_dir_path(dirname(__FILE__)) . 'class-hvac-secure-storage.php';
}
// Load credentials from WordPress options using secure storage (encrypted)
$this->client_id = HVAC_Secure_Storage::get_credential('hvac_zoho_client_id', '');
$this->client_secret = HVAC_Secure_Storage::get_credential('hvac_zoho_client_secret', '');
$this->refresh_token = HVAC_Secure_Storage::get_credential('hvac_zoho_refresh_token', '');
// Load credentials from WordPress options (new approach)
$this->client_id = get_option('hvac_zoho_client_id', '');
$this->client_secret = get_option('hvac_zoho_client_secret', '');
$this->refresh_token = get_option('hvac_zoho_refresh_token', '');
$this->redirect_uri = get_site_url() . '/oauth/callback';
// Fallback to config file if options are empty (backward compatibility)
@ -53,59 +48,20 @@ class HVAC_Zoho_CRM_Auth {
/**
* Generate authorization URL for initial setup
*
* @return string Authorization URL with CSRF state parameter
*/
public function get_authorization_url() {
// Generate secure state parameter for CSRF protection
$state = $this->generate_oauth_state();
$params = array(
'scope' => 'ZohoCRM.settings.ALL,ZohoCRM.modules.ALL,ZohoCRM.users.ALL,ZohoCRM.org.ALL,ZohoCRM.bulk.READ',
'client_id' => $this->client_id,
'response_type' => 'code',
'access_type' => 'offline',
'redirect_uri' => $this->redirect_uri,
'prompt' => 'consent',
'state' => $state
'prompt' => 'consent'
);
return 'https://accounts.zoho.com/oauth/v2/auth?' . http_build_query($params);
}
/**
* Generate and store OAuth state parameter for CSRF protection
*
* @return string Generated state token
*/
public function generate_oauth_state() {
$state = wp_generate_password(32, false);
set_transient('hvac_zoho_oauth_state', $state, 600); // 10 minute expiry
return $state;
}
/**
* Validate OAuth state parameter
*
* @param string $state State parameter from callback
* @return bool True if state is valid
*/
public function validate_oauth_state($state) {
$stored_state = get_transient('hvac_zoho_oauth_state');
if (empty($stored_state) || empty($state)) {
return false;
}
// Use timing-safe comparison
$valid = hash_equals($stored_state, $state);
// Delete the state after validation (one-time use)
delete_transient('hvac_zoho_oauth_state');
return $valid;
}
/**
* Exchange authorization code for tokens
*/
@ -359,31 +315,31 @@ class HVAC_Zoho_CRM_Auth {
}
/**
* Save tokens to WordPress options using secure storage
* Save tokens to WordPress options
*/
private function save_tokens() {
HVAC_Secure_Storage::store_credential('hvac_zoho_refresh_token', $this->refresh_token);
update_option('hvac_zoho_refresh_token', $this->refresh_token);
$this->save_access_token();
}
/**
* Save access token using secure storage
* Save access token
*/
private function save_access_token() {
HVAC_Secure_Storage::store_credential('hvac_zoho_access_token', $this->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 using secure storage
* Load access token from WordPress options
*/
private function load_access_token() {
$this->access_token = HVAC_Secure_Storage::get_credential('hvac_zoho_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 = HVAC_Secure_Storage::get_credential('hvac_zoho_refresh_token', '');
$this->refresh_token = get_option('hvac_zoho_refresh_token');
}
}
@ -407,46 +363,16 @@ class HVAC_Zoho_CRM_Auth {
* Log debug messages
*/
private function log_debug($message) {
// Sanitize message to remove sensitive data
$sanitized = $this->sanitize_log_message($message);
if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE && defined('ZOHO_LOG_FILE')) {
error_log('[' . date('Y-m-d H:i:s') . '] DEBUG: ' . $sanitized . PHP_EOL, 3, ZOHO_LOG_FILE);
error_log('[' . date('Y-m-d H:i:s') . '] DEBUG: ' . $message . PHP_EOL, 3, ZOHO_LOG_FILE);
}
// Also log to WordPress debug log if available
if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE && defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
error_log('[ZOHO CRM DEBUG] ' . $sanitized);
error_log('[ZOHO CRM DEBUG] ' . $message);
}
}
/**
* Sanitize log messages to mask sensitive credentials
*
* @param string $message Log message
* @return string Sanitized message
*/
private function sanitize_log_message($message) {
// Mask client_id, client_secret, access_token, refresh_token patterns
$patterns = array(
'/(client[_-]?(id|secret)[\s:=]+)([a-zA-Z0-9._-]{10,})/i',
'/(access[_-]?token[\s:=]+)([a-zA-Z0-9._-]{10,})/i',
'/(refresh[_-]?token[\s:=]+)([a-zA-Z0-9._-]{10,})/i',
'/(authorization[\s:]+)(Zoho-oauthtoken\s+[a-zA-Z0-9._-]+)/i',
'/("(client_id|client_secret|access_token|refresh_token)"[\s:]+")[^"]+(")/i',
);
$replacements = array(
'$1***MASKED***',
'$1***MASKED***',
'$1***MASKED***',
'$1Zoho-oauthtoken ***MASKED***',
'$1***MASKED***$3',
);
return preg_replace($patterns, $replacements, $message);
}
/**
* Get the last error message
*

View file

@ -1,389 +0,0 @@
<?php
/**
* Zoho CRM Scheduled Sync Handler
*
* Manages WP-Cron based scheduled sync of WordPress data to Zoho CRM.
*
* @package HVACCommunityEvents
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Scheduled Sync Class
*/
class HVAC_Zoho_Scheduled_Sync {
/**
* Instance of this class
*
* @var HVAC_Zoho_Scheduled_Sync
*/
private static $instance = null;
/**
* Cron hook name
*/
const CRON_HOOK = 'hvac_zoho_scheduled_sync';
/**
* Option names
*/
const OPTION_ENABLED = 'hvac_zoho_auto_sync';
const OPTION_INTERVAL = 'hvac_zoho_sync_frequency';
const OPTION_LAST_SYNC = 'hvac_zoho_last_sync_time';
const OPTION_LAST_RESULT = 'hvac_zoho_last_sync_result';
/**
* Get instance of this class
*
* @return HVAC_Zoho_Scheduled_Sync
*/
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Register custom cron schedules
add_filter('cron_schedules', array($this, 'register_cron_schedules'));
// Register the cron action
add_action(self::CRON_HOOK, array($this, 'run_scheduled_sync'));
// Check if we need to reschedule on settings change
// Hook into both add_option (first time) and update_option (subsequent changes)
add_action('add_option_' . self::OPTION_ENABLED, array($this, 'on_setting_added'), 10, 2);
add_action('update_option_' . self::OPTION_ENABLED, array($this, 'on_setting_change'), 10, 2);
add_action('update_option_' . self::OPTION_INTERVAL, array($this, 'on_interval_change'), 10, 2);
}
/**
* Register custom cron schedules
*
* @param array $schedules Existing schedules
* @return array Modified schedules
*/
public function register_cron_schedules($schedules) {
$schedules['every_5_minutes'] = array(
'interval' => 5 * MINUTE_IN_SECONDS,
'display' => __('Every 5 Minutes', 'hvac-community-events')
);
$schedules['every_15_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => __('Every 15 Minutes', 'hvac-community-events')
);
$schedules['every_30_minutes'] = array(
'interval' => 30 * MINUTE_IN_SECONDS,
'display' => __('Every 30 Minutes', 'hvac-community-events')
);
$schedules['every_6_hours'] = array(
'interval' => 6 * HOUR_IN_SECONDS,
'display' => __('Every 6 Hours', 'hvac-community-events')
);
return $schedules;
}
/**
* Schedule the sync cron event
*
* @param string|null $interval Optional interval override
* @return bool True if scheduled successfully
*/
public function schedule_sync($interval = null) {
// Unschedule any existing event first
$this->unschedule_sync();
// Get interval from option if not provided
if (null === $interval) {
$interval = get_option(self::OPTION_INTERVAL, 'every_5_minutes');
}
// Schedule the event
$result = wp_schedule_event(time(), $interval, self::CRON_HOOK);
if ($result !== false) {
if (class_exists('HVAC_Logger')) {
HVAC_Logger::info("Scheduled Zoho sync with interval: {$interval}", 'ZohoScheduledSync');
}
return true;
}
return false;
}
/**
* Unschedule the sync cron event
*
* @return bool True if unscheduled successfully
*/
public function unschedule_sync() {
$timestamp = wp_next_scheduled(self::CRON_HOOK);
if ($timestamp) {
wp_unschedule_event($timestamp, self::CRON_HOOK);
if (class_exists('HVAC_Logger')) {
HVAC_Logger::info('Unscheduled Zoho sync', 'ZohoScheduledSync');
}
return true;
}
return false;
}
/**
* Check if sync is currently scheduled
*
* @return bool
*/
public function is_scheduled() {
return wp_next_scheduled(self::CRON_HOOK) !== false;
}
/**
* Get next scheduled sync time
*
* @return int|false Timestamp or false if not scheduled
*/
public function get_next_scheduled() {
return wp_next_scheduled(self::CRON_HOOK);
}
/**
* Handle first-time option creation
*
* @param string $option Option name
* @param mixed $value Option value
*/
public function on_setting_added($option, $value) {
if ($value === '1' || $value === 1 || $value === true) {
$this->schedule_sync();
}
}
/**
* Handle setting enabled/disabled change
*
* @param mixed $old_value Old option value
* @param mixed $new_value New option value
*/
public function on_setting_change($old_value, $new_value) {
if ($new_value === '1' || $new_value === 1 || $new_value === true) {
$this->schedule_sync();
} else {
$this->unschedule_sync();
}
}
/**
* Handle interval change
*
* @param mixed $old_value Old interval value
* @param mixed $new_value New interval value
*/
public function on_interval_change($old_value, $new_value) {
// Only reschedule if currently enabled
if (get_option(self::OPTION_ENABLED) === '1') {
$this->schedule_sync($new_value);
}
}
/**
* Run the scheduled sync
*
* This is the main cron callback that syncs all data types.
*/
public function run_scheduled_sync() {
$start_time = time();
if (class_exists('HVAC_Logger')) {
HVAC_Logger::info('Starting scheduled Zoho sync', 'ZohoScheduledSync');
}
// Get last sync time for incremental sync
$last_sync = $this->get_last_sync_time();
// Initialize results
$results = array(
'started_at' => date('Y-m-d H:i:s', $start_time),
'last_sync_from' => $last_sync ? date('Y-m-d H:i:s', $last_sync) : 'Never',
'events' => null,
'users' => null,
'attendees' => null,
'rsvps' => null,
'purchases' => null,
'errors' => array(),
);
try {
// Load dependencies
require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-sync.php';
$sync = new HVAC_Zoho_Sync();
// Sync each type with since_timestamp for incremental sync
// Events
$results['events'] = $this->sync_all_batches($sync, 'sync_events', $last_sync);
// Users/Trainers
$results['users'] = $this->sync_all_batches($sync, 'sync_users', $last_sync);
// Attendees
$results['attendees'] = $this->sync_all_batches($sync, 'sync_attendees', $last_sync);
// RSVPs
$results['rsvps'] = $this->sync_all_batches($sync, 'sync_rsvps', $last_sync);
// Purchases/Orders
$results['purchases'] = $this->sync_all_batches($sync, 'sync_purchases', $last_sync);
} catch (Exception $e) {
$results['errors'][] = $e->getMessage();
if (class_exists('HVAC_Logger')) {
HVAC_Logger::error('Scheduled sync error: ' . $e->getMessage(), 'ZohoScheduledSync');
}
}
// Update last sync time
$this->set_last_sync_time($start_time);
// Calculate totals
$total_synced = 0;
$total_failed = 0;
foreach (array('events', 'users', 'attendees', 'rsvps', 'purchases') as $type) {
if (isset($results[$type]['synced'])) {
$total_synced += $results[$type]['synced'];
}
if (isset($results[$type]['failed'])) {
$total_failed += $results[$type]['failed'];
}
}
$results['completed_at'] = date('Y-m-d H:i:s');
$results['duration_seconds'] = time() - $start_time;
$results['total_synced'] = $total_synced;
$results['total_failed'] = $total_failed;
// Save last result
update_option(self::OPTION_LAST_RESULT, $results);
if (class_exists('HVAC_Logger')) {
HVAC_Logger::info("Scheduled sync completed: {$total_synced} synced, {$total_failed} failed", 'ZohoScheduledSync');
}
return $results;
}
/**
* Sync all batches for a given sync method
*
* @param HVAC_Zoho_Sync $sync Sync instance
* @param string $method Method name (e.g., 'sync_events')
* @param int|null $since_timestamp Optional timestamp for incremental sync
* @return array Aggregated results
*/
private function sync_all_batches($sync, $method, $since_timestamp = null) {
$offset = 0;
$limit = 50;
$aggregated = array(
'total' => 0,
'synced' => 0,
'failed' => 0,
'errors' => array(),
);
do {
// Call the sync method with since_timestamp support
$result = $sync->$method($offset, $limit, $since_timestamp);
// Aggregate results
$aggregated['total'] = $result['total'] ?? 0;
$aggregated['synced'] += $result['synced'] ?? 0;
$aggregated['failed'] += $result['failed'] ?? 0;
if (!empty($result['errors'])) {
$aggregated['errors'] = array_merge($aggregated['errors'], $result['errors']);
}
// Check for more batches
$has_more = $result['has_more'] ?? false;
$offset = $result['next_offset'] ?? ($offset + $limit);
} while ($has_more);
return $aggregated;
}
/**
* Get the last sync timestamp
*
* @return int|null Timestamp or null if never synced
*/
public function get_last_sync_time() {
$time = get_option(self::OPTION_LAST_SYNC);
return $time ? (int) $time : null;
}
/**
* Set the last sync timestamp
*
* @param int $timestamp Timestamp
*/
public function set_last_sync_time($timestamp) {
update_option(self::OPTION_LAST_SYNC, $timestamp);
}
/**
* Get the last sync result
*
* @return array|null Result array or null
*/
public function get_last_sync_result() {
return get_option(self::OPTION_LAST_RESULT);
}
/**
* Get sync status summary
*
* @return array Status information
*/
public function get_status() {
$is_enabled = get_option(self::OPTION_ENABLED) === '1';
$interval = get_option(self::OPTION_INTERVAL, 'every_5_minutes');
$last_sync = $this->get_last_sync_time();
$next_sync = $this->get_next_scheduled();
$last_result = $this->get_last_sync_result();
return array(
'enabled' => $is_enabled,
'interval' => $interval,
'is_scheduled' => $this->is_scheduled(),
'last_sync_time' => $last_sync,
'last_sync_formatted' => $last_sync ? date('Y-m-d H:i:s', $last_sync) : 'Never',
'next_sync_time' => $next_sync,
'next_sync_formatted' => $next_sync ? date('Y-m-d H:i:s', $next_sync) : 'Not scheduled',
'last_result' => $last_result,
);
}
/**
* Run sync manually (for "Run Now" button)
*
* @return array Sync results
*/
public function run_now() {
return $this->run_scheduled_sync();
}
}

View file

@ -65,11 +65,17 @@ class HVAC_Zoho_Sync {
'staging_mode' => $this->is_staging
);
// Get all published events (past and future - 'custom' bypasses date filtering)
// Get all published events
$events = tribe_get_events(array(
'posts_per_page' => -1,
'eventDisplay' => 'custom',
'start_date' => '2020-01-01', // Include all historical events
'eventDisplay' => 'list',
'meta_query' => array(
array(
'key' => '_hvac_event_type',
'value' => 'trainer',
'compare' => '='
)
)
));
$results['total'] = count($events);
@ -150,9 +156,9 @@ class HVAC_Zoho_Sync {
'staging_mode' => $this->is_staging
);
// Get trainers (hvac_trainer and hvac_master_trainer roles)
// Get trainers and attendees
$users = get_users(array(
'role__in' => array('hvac_trainer', 'hvac_master_trainer'),
'role__in' => array('trainer', 'trainee'),
'meta_query' => array(
'relation' => 'OR',
array(
@ -237,7 +243,7 @@ class HVAC_Zoho_Sync {
}
/**
* Sync Event Tickets orders to Zoho Invoices
* Sync ticket purchases to Zoho Invoices
*
* @return array Sync results
*/
@ -250,20 +256,16 @@ class HVAC_Zoho_Sync {
'staging_mode' => $this->is_staging
);
// Get Tickets Commerce orders (tec_tc_order post type)
$orders = get_posts(array(
'post_type' => 'tec_tc_order',
'posts_per_page' => -1,
'post_status' => 'tec-tc-completed',
// 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 ($results['total'] === 0) {
$results['message'] = 'No completed ticket orders found.';
return $results;
}
// If staging mode, simulate the sync
if ($this->is_staging) {
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
@ -271,12 +273,11 @@ class HVAC_Zoho_Sync {
$results['test_data'] = array();
foreach ($orders as $order) {
$invoice_data = $this->prepare_tc_invoice_data($order);
$invoice_data = $this->prepare_invoice_data($order);
$results['test_data'][] = array(
'order_id' => $order->ID,
'purchaser_email' => get_post_meta($order->ID, '_tec_tc_order_purchaser_email', true),
'gateway' => get_post_meta($order->ID, '_tec_tc_order_gateway', true),
'date' => $order->post_date,
'order_id' => $order->get_id(),
'order_number' => $order->get_order_number(),
'order_total' => $order->get_total(),
'zoho_data' => $invoice_data
);
}
@ -292,327 +293,44 @@ class HVAC_Zoho_Sync {
foreach ($orders as $order) {
try {
$invoice_data = $this->prepare_tc_invoice_data($order);
$invoice_data = $this->prepare_invoice_data($order);
// Check if invoice already exists in Zoho (by WordPress Order ID)
$search_response = $this->auth->make_api_request(
'/Invoices/search?criteria=(WordPress_Order_ID:equals:' . $order->ID . ')',
'GET'
);
// 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'];
$this->auth->make_api_request("/Invoices/{$invoice_id}", 'PUT', array(
$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('/Invoices', 'POST', array(
$create_response = $this->auth->make_api_request('POST', '/crm/v2/Invoices', array(
'data' => array($invoice_data)
));
if (!empty($create_response['data'][0]['details']['id'])) {
$invoice_id = $create_response['data'][0]['details']['id'];
}
}
$results['synced']++;
// Update order meta with Zoho ID
if (isset($invoice_id)) {
update_post_meta($order->ID, '_zoho_invoice_id', $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->ID, $e->getMessage());
$results['errors'][] = sprintf('Order %s: %s', $order->get_id(), $e->getMessage());
}
}
return $results;
}
/**
* Sync ticket attendees to Zoho Contacts + Campaign Members
*
* @return array Sync results
*/
public function sync_attendees() {
$results = array(
'total' => 0,
'synced' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging,
'contacts_created' => 0,
'campaign_members_created' => 0
);
// Get all ticket attendees (Tickets Commerce)
$attendees = get_posts(array(
'post_type' => 'tec_tc_attendee',
'posts_per_page' => -1,
'post_status' => 'any',
));
// Also get PayPal attendees if any
$tpp_attendees = get_posts(array(
'post_type' => 'tribe_tpp_attendees',
'posts_per_page' => -1,
'post_status' => 'any',
));
$all_attendees = array_merge($attendees, $tpp_attendees);
$results['total'] = count($all_attendees);
if ($results['total'] === 0) {
$results['message'] = 'No ticket attendees found.';
return $results;
}
// If staging mode, simulate the sync
if ($this->is_staging) {
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
$results['synced'] = $results['total'];
$results['test_data'] = array();
foreach ($all_attendees as $attendee) {
$attendee_data = $this->prepare_attendee_data($attendee);
$results['test_data'][] = $attendee_data;
}
return $results;
}
// Production sync
if (!$this->is_sync_allowed()) {
$results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.';
return $results;
}
foreach ($all_attendees as $attendee) {
try {
$attendee_data = $this->prepare_attendee_data($attendee);
if (empty($attendee_data['email'])) {
$results['failed']++;
$results['errors'][] = sprintf('Attendee %s: No email address', $attendee->ID);
continue;
}
// Step 1: Create/Update Contact
$contact_id = $this->ensure_contact_exists($attendee_data);
if ($contact_id) {
$results['contacts_created']++;
}
// Step 2: Create Campaign Member (link Contact to Campaign)
if ($contact_id && !empty($attendee_data['event_id'])) {
$campaign_id = get_post_meta($attendee_data['event_id'], '_zoho_campaign_id', true);
if ($campaign_id) {
$this->create_campaign_member($contact_id, $campaign_id, 'Attended');
$results['campaign_members_created']++;
}
}
$results['synced']++;
// Update attendee meta with Zoho Contact ID
update_post_meta($attendee->ID, '_zoho_contact_id', $contact_id);
} catch (Exception $e) {
$results['failed']++;
$results['errors'][] = sprintf('Attendee %s: %s', $attendee->ID, $e->getMessage());
}
}
return $results;
}
/**
* Sync RSVPs to Zoho Leads + Campaign Members
*
* @return array Sync results
*/
public function sync_rsvps() {
$results = array(
'total' => 0,
'synced' => 0,
'failed' => 0,
'errors' => array(),
'staging_mode' => $this->is_staging,
'leads_created' => 0,
'campaign_members_created' => 0
);
// Get RSVP attendees
$rsvps = get_posts(array(
'post_type' => 'tribe_rsvp_attendees',
'posts_per_page' => -1,
'post_status' => 'any',
));
$results['total'] = count($rsvps);
if ($results['total'] === 0) {
$results['message'] = 'No RSVP attendees found.';
return $results;
}
// If staging mode, simulate the sync
if ($this->is_staging) {
$results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.';
$results['synced'] = $results['total'];
$results['test_data'] = array();
foreach ($rsvps as $rsvp) {
$rsvp_data = $this->prepare_rsvp_data($rsvp);
$results['test_data'][] = $rsvp_data;
}
return $results;
}
// Production sync
if (!$this->is_sync_allowed()) {
$results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.';
return $results;
}
foreach ($rsvps as $rsvp) {
try {
$rsvp_data = $this->prepare_rsvp_data($rsvp);
if (empty($rsvp_data['email'])) {
$results['failed']++;
$results['errors'][] = sprintf('RSVP %s: No email address', $rsvp->ID);
continue;
}
// Step 1: Create/Update Lead
$lead_id = $this->ensure_lead_exists($rsvp_data);
if ($lead_id) {
$results['leads_created']++;
}
// Step 2: Create Campaign Member (link Lead to Campaign)
if ($lead_id && !empty($rsvp_data['event_id'])) {
$campaign_id = get_post_meta($rsvp_data['event_id'], '_zoho_campaign_id', true);
if ($campaign_id) {
$this->create_campaign_member($lead_id, $campaign_id, 'Responded', 'Leads');
$results['campaign_members_created']++;
}
}
$results['synced']++;
// Update RSVP meta with Zoho Lead ID
update_post_meta($rsvp->ID, '_zoho_lead_id', $lead_id);
} catch (Exception $e) {
$results['failed']++;
$results['errors'][] = sprintf('RSVP %s: %s', $rsvp->ID, $e->getMessage());
}
}
return $results;
}
/**
* Ensure a Contact exists in Zoho (create or get existing)
*
* @param array $data Attendee data
* @return string|null Zoho Contact ID
*/
private function ensure_contact_exists($data) {
// Search for existing contact by email
$search_response = $this->auth->make_api_request(
'/Contacts/search?criteria=(Email:equals:' . urlencode($data['email']) . ')',
'GET'
);
if (!empty($search_response['data'])) {
return $search_response['data'][0]['id'];
}
// Create new contact
$contact_data = array(
'First_Name' => $data['first_name'] ?: 'Unknown',
'Last_Name' => $data['last_name'] ?: 'Attendee',
'Email' => $data['email'],
'Lead_Source' => 'Event Tickets',
'Contact_Type' => 'Attendee',
'WordPress_Attendee_ID' => $data['attendee_id'],
);
$create_response = $this->auth->make_api_request('/Contacts', 'POST', array(
'data' => array($contact_data)
));
if (!empty($create_response['data'][0]['details']['id'])) {
return $create_response['data'][0]['details']['id'];
}
return null;
}
/**
* Ensure a Lead exists in Zoho (create or get existing)
*
* @param array $data RSVP data
* @return string|null Zoho Lead ID
*/
private function ensure_lead_exists($data) {
// Search for existing lead by email
$search_response = $this->auth->make_api_request(
'/Leads/search?criteria=(Email:equals:' . urlencode($data['email']) . ')',
'GET'
);
if (!empty($search_response['data'])) {
return $search_response['data'][0]['id'];
}
// Create new lead
$lead_data = array(
'First_Name' => $data['first_name'] ?: 'Unknown',
'Last_Name' => $data['last_name'] ?: 'RSVP',
'Email' => $data['email'],
'Lead_Source' => 'Event RSVP',
'Lead_Status' => 'Contacted',
'WordPress_RSVP_ID' => $data['rsvp_id'],
);
$create_response = $this->auth->make_api_request('/Leads', 'POST', array(
'data' => array($lead_data)
));
if (!empty($create_response['data'][0]['details']['id'])) {
return $create_response['data'][0]['details']['id'];
}
return null;
}
/**
* Create a Campaign Member (link Contact/Lead to Campaign)
*
* @param string $record_id Contact or Lead ID
* @param string $campaign_id Campaign ID
* @param string $status Member status (Invited, Responded, Attended, etc.)
* @param string $type 'Contacts' or 'Leads'
*/
private function create_campaign_member($record_id, $campaign_id, $status = 'Attended', $type = 'Contacts') {
$endpoint = "/Campaigns/{$campaign_id}/{$type}/{$record_id}";
$this->auth->make_api_request($endpoint, 'PUT', array(
'data' => array(
array('Member_Status' => $status)
)
));
}
/**
* Prepare campaign data for Zoho
*
@ -647,14 +365,7 @@ class HVAC_Zoho_Sync {
* @return array Contact data
*/
private function prepare_contact_data($user) {
// Map WordPress roles to Zoho Contact_Type
if (in_array('hvac_master_trainer', $user->roles)) {
$role = 'Master Trainer';
} elseif (in_array('hvac_trainer', $user->roles)) {
$role = 'Trainer';
} else {
$role = 'Trainee';
}
$role = in_array('trainer', $user->roles) ? 'Trainer' : 'Trainee';
return array(
'First_Name' => get_user_meta($user->ID, 'first_name', true),
@ -673,163 +384,45 @@ class HVAC_Zoho_Sync {
}
/**
* Prepare invoice data for Tickets Commerce order
* Prepare invoice data for Zoho
*
* @param WP_Post $order TC Order post object
* @param WC_Order $order Order object
* @return array Invoice data
*/
private function prepare_tc_invoice_data($order) {
$purchaser_email = get_post_meta($order->ID, '_tec_tc_order_purchaser_email', true);
$purchaser_name = get_post_meta($order->ID, '_tec_tc_order_purchaser_name', true);
$gateway = get_post_meta($order->ID, '_tec_tc_order_gateway', true);
$gateway_order_id = get_post_meta($order->ID, '_tec_tc_order_gateway_order_id', true);
$order_items = get_post_meta($order->ID, '_tec_tc_order_items', true);
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();
// Parse order items to get event info and line items
$items = array();
$event_titles = array();
$total = 0;
if (is_array($order_items)) {
foreach ($order_items as $item) {
$event_id = isset($item['event_id']) ? $item['event_id'] : 0;
$ticket_id = isset($item['ticket_id']) ? $item['ticket_id'] : 0;
$quantity = isset($item['quantity']) ? intval($item['quantity']) : 1;
$price = isset($item['price']) ? floatval($item['price']) : 0;
if ($event_id) {
$event_titles[] = get_the_title($event_id);
}
$ticket_name = $ticket_id ? get_the_title($ticket_id) : 'Event Ticket';
$items[] = array(
'product' => array('name' => $ticket_name),
'quantity' => $quantity,
'list_price' => $price,
'total' => $price * $quantity
);
$total += $price * $quantity;
}
// Get contact ID from Zoho
$contact_id = null;
if ($customer) {
$contact_id = get_user_meta($customer->ID, '_zoho_contact_id', true);
}
$event_summary = !empty($event_titles) ? implode(', ', array_unique($event_titles)) : 'Event Tickets';
$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(
'Subject' => "Ticket Purchase - {$event_summary}",
'Invoice_Date' => date('Y-m-d', strtotime($order->post_date)),
'Invoice_Number' => $order->get_order_number(),
'Invoice_Date' => $order->get_date_created()->format('Y-m-d'),
'Status' => 'Paid',
'Account_Name' => $purchaser_name,
'Description' => "Payment via {$gateway}. Transaction: {$gateway_order_id}",
'Grand_Total' => $total,
'WordPress_Order_ID' => $order->ID,
'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
);
}
/**
* Prepare attendee data from Event Tickets attendee post
*
* @param WP_Post $attendee Attendee post object
* @return array Attendee data
*/
private function prepare_attendee_data($attendee) {
$post_type = $attendee->post_type;
// Handle different attendee post types
if ($post_type === 'tec_tc_attendee') {
// Tickets Commerce attendee (meta keys: _tec_tickets_commerce_*)
$event_id = get_post_meta($attendee->ID, '_tec_tickets_commerce_event', true);
$ticket_id = get_post_meta($attendee->ID, '_tec_tickets_commerce_ticket', true);
$order_id = get_post_meta($attendee->ID, '_tec_tickets_commerce_order', true);
$full_name = get_post_meta($attendee->ID, '_tec_tickets_commerce_full_name', true);
$email = get_post_meta($attendee->ID, '_tec_tickets_commerce_email', true);
$checkin = get_post_meta($attendee->ID, '_tec_tickets_commerce_checked_in', true);
} else {
// PayPal or other attendee types (tribe_tpp_attendees)
$event_id = get_post_meta($attendee->ID, '_tribe_tpp_event', true);
$ticket_id = get_post_meta($attendee->ID, '_tribe_tpp_product', true);
$order_id = get_post_meta($attendee->ID, '_tribe_tpp_order', true);
$full_name = get_post_meta($attendee->ID, '_tribe_tickets_full_name', true);
$email = get_post_meta($attendee->ID, '_tribe_tickets_email', true);
$checkin = get_post_meta($attendee->ID, '_tribe_tpp_checkin', true);
}
// Parse full name into first/last
$name_parts = explode(' ', trim($full_name), 2);
$first_name = isset($name_parts[0]) ? $name_parts[0] : '';
$last_name = isset($name_parts[1]) ? $name_parts[1] : '';
$event_title = $event_id ? get_the_title($event_id) : '';
$ticket_name = $ticket_id ? get_the_title($ticket_id) : '';
return array(
'attendee_id' => $attendee->ID,
'post_type' => $post_type,
'event_id' => $event_id,
'event_title' => $event_title,
'ticket_id' => $ticket_id,
'ticket_name' => $ticket_name,
'order_id' => $order_id,
'full_name' => $full_name,
'first_name' => $first_name,
'last_name' => $last_name,
'email' => $email,
'checked_in' => !empty($checkin),
'zoho_contact' => array(
'First_Name' => $first_name ?: 'Unknown',
'Last_Name' => $last_name ?: 'Attendee',
'Email' => $email,
'Lead_Source' => 'Event Tickets',
'Contact_Type' => 'Attendee',
),
'zoho_campaign_member' => array(
'Member_Status' => !empty($checkin) ? 'Attended' : 'Registered',
)
);
}
/**
* Prepare RSVP data from Event Tickets RSVP attendee post
*
* @param WP_Post $rsvp RSVP attendee post object
* @return array RSVP data
*/
private function prepare_rsvp_data($rsvp) {
$event_id = get_post_meta($rsvp->ID, '_tribe_rsvp_event', true);
$ticket_id = get_post_meta($rsvp->ID, '_tribe_rsvp_product', true);
$full_name = get_post_meta($rsvp->ID, '_tribe_rsvp_full_name', true);
$email = get_post_meta($rsvp->ID, '_tribe_rsvp_email', true);
$rsvp_status = get_post_meta($rsvp->ID, '_tribe_rsvp_status', true); // yes, no
// Parse full name into first/last
$name_parts = explode(' ', trim($full_name), 2);
$first_name = isset($name_parts[0]) ? $name_parts[0] : '';
$last_name = isset($name_parts[1]) ? $name_parts[1] : '';
$event_title = $event_id ? get_the_title($event_id) : '';
return array(
'rsvp_id' => $rsvp->ID,
'event_id' => $event_id,
'event_title' => $event_title,
'ticket_id' => $ticket_id,
'full_name' => $full_name,
'first_name' => $first_name,
'last_name' => $last_name,
'email' => $email,
'rsvp_status' => $rsvp_status,
'zoho_lead' => array(
'First_Name' => $first_name ?: 'Unknown',
'Last_Name' => $last_name ?: 'RSVP',
'Email' => $email,
'Lead_Source' => 'Event RSVP',
'Lead_Status' => $rsvp_status === 'yes' ? 'Contacted' : 'Not Interested',
),
'zoho_campaign_member' => array(
'Member_Status' => $rsvp_status === 'yes' ? 'Responded' : 'Not Responded',
)
);
}
}
?>

View file

@ -14,15 +14,22 @@ const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
// Import WordPress error detector
const WordPressErrorDetector = require(path.join(__dirname, 'tests', 'framework', 'utils', 'WordPressErrorDetector'));
// Test configuration
const BASE_URL = 'https://upskill-staging.measurequick.com';
const SCREENSHOTS_DIR = path.join(__dirname, 'test-evidence');
// Test credentials
// Test credentials (if available)
const TEST_CREDENTIALS = {
trainer: {
username: process.env.TRAINER_USERNAME || 'test-trainer',
password: process.env.TRAINER_PASSWORD || 'test-password'
},
master: {
username: process.env.MASTER_USERNAME || 'test_master',
password: process.env.MASTER_PASSWORD || 'Test123!'
username: process.env.MASTER_USERNAME || 'test-master',
password: process.env.MASTER_PASSWORD || 'test-password'
}
};
@ -35,7 +42,7 @@ const TRAINER_PAGES = [
];
const MASTER_TRAINER_PAGES = [
'/master-trainer/dashboard/',
'/master-trainer/google-sheets/',
'/master-trainer/announcements/',
'/master-trainer/pending-approvals/',
'/master-trainer/trainers/'
@ -64,13 +71,9 @@ class ComprehensiveValidator {
}
// Launch browser
const headlessMode = process.env.HEADLESS !== 'false'; // Set HEADLESS=false to run in headed mode
console.log(` Browser mode: ${headlessMode ? 'headless' : 'headed (visible)'}`);
this.browser = await chromium.launch({
headless: headlessMode,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
slowMo: headlessMode ? 0 : 100 // Slow down actions in headed mode for visibility
headless: true, // Headless mode for server environment
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
this.page = await this.browser.newPage();
@ -80,8 +83,6 @@ class ComprehensiveValidator {
// Listen for console messages
this.page.on('console', msg => {
if (msg.type() === 'error') {
// Ignore specific known/expected errors during negative testing
if (msg.text().includes('401') || msg.text().includes('403')) return;
console.log('🔥 Console Error:', msg.text());
this.results.overall.errors.push(`Console Error: ${msg.text()}`);
}
@ -96,74 +97,7 @@ class ComprehensiveValidator {
return filename;
}
async login() {
console.log('\n🔑 Logging in as Master Trainer...');
console.log(` Credentials: ${TEST_CREDENTIALS.master.username} / ${'*'.repeat(TEST_CREDENTIALS.master.password.length)}`);
try {
await this.page.goto(`${BASE_URL}/community-login/`, { waitUntil: 'domcontentloaded' });
// Wait for login form elements using IDs
const loginInput = await this.page.waitForSelector('#user_login', { timeout: 10000 }).catch(() => null);
const passInput = await this.page.waitForSelector('#user_pass', { timeout: 1000 }).catch(() => null);
const submitButton = await this.page.waitForSelector('#wp-submit', { timeout: 1000 }).catch(() => null);
if (!loginInput || !passInput || !submitButton) {
console.log(' ⚠️ Login form elements not found - may already be logged in');
const currentUrl = this.page.url();
console.log(` Current URL: ${currentUrl}`);
return currentUrl.includes('dashboard');
}
// Fill in credentials
console.log(' Filling credentials...');
await this.page.fill('#user_login', TEST_CREDENTIALS.master.username);
await this.page.fill('#user_pass', TEST_CREDENTIALS.master.password);
// Click submit and wait for navigation
console.log(' Clicking submit (#wp-submit)...');
await Promise.all([
this.page.waitForNavigation({ timeout: 15000 }).catch(e => console.log(` Navigation timeout: ${e.message}`)),
this.page.click('#wp-submit')
]);
// Wait a bit more for any redirects
await this.page.waitForTimeout(2000);
const finalUrl = this.page.url();
console.log(` Post-login URL: ${finalUrl}`);
// Check if we're on a dashboard or authenticated page
const isOnDashboard = finalUrl.includes('dashboard') || finalUrl.includes('master-trainer') || finalUrl.includes('trainer/');
const isOnLoginPage = finalUrl.includes('login') || finalUrl.includes('wp-login');
// Additional check: look for WordPress logged-in class or dashboard elements
const hasLoggedInClass = await this.page.evaluate(() => document.body.classList.contains('logged-in')).catch(() => false);
const hasDashboardElement = await this.page.$('.hvac-dashboard, .master-dashboard, #wpadminbar').then(el => !!el).catch(() => false);
console.log(` Dashboard URL: ${isOnDashboard}`);
console.log(` Login page: ${isOnLoginPage}`);
console.log(` Logged-in class: ${hasLoggedInClass}`);
console.log(` Dashboard element: ${hasDashboardElement}`);
if (isOnDashboard || hasLoggedInClass || hasDashboardElement) {
console.log(' ✅ Login successful');
await this.takeScreenshot('login-success');
return true;
} else {
console.log(' ❌ Login failed - still on login page or unexpected location');
await this.takeScreenshot('login-failed');
return false;
}
} catch (error) {
console.error(` 💥 Login error: ${error.message}`);
await this.takeScreenshot('login-error').catch(() => { });
return false;
}
}
async testPageExists(url, pageName, requiresAuth = false) {
async testPageExists(url, pageName) {
console.log(`\n🧪 Testing: ${pageName}`);
console.log(` URL: ${BASE_URL}${url}`);
@ -192,16 +126,9 @@ class ComprehensiveValidator {
// Check for WordPress login redirect
const currentUrl = this.page.url();
if (currentUrl.includes('wp-login') || currentUrl.includes('login') || currentUrl.includes('community-login')) {
if (requiresAuth) {
result.authenticated = false;
result.errors.push('Page redirected to login');
console.log(` ❌ FAIL: Redirected to login`);
} else {
// Some pages might redirect if public access isn't allowed, which might be valid
result.authenticated = false;
console.log(` ⚠️ Redirected to login (Public access checked)`);
}
if (currentUrl.includes('wp-login') || currentUrl.includes('login')) {
result.authenticated = false;
result.errors.push('Page requires authentication - redirected to login');
} else {
result.authenticated = true;
@ -249,8 +176,18 @@ class ComprehensiveValidator {
try {
await this.page.goto(`${BASE_URL}${url}`, { waitUntil: 'networkidle' });
// Check for single column layout (look for main content container)
const mainContent = await this.page.$('.hvac-single-column, .single-column, main, .main-content');
if (mainContent) {
result.singleColumn = true;
console.log(` ✅ Single column layout detected`);
} else {
result.errors.push('Single column layout not detected');
console.log(` ❌ Single column layout not found`);
}
// Check for navigation/breadcrumbs
const navigation = await this.page.$('.breadcrumb, .navigation, nav, .hvac-navigation, .breadcrumbs');
const navigation = await this.page.$('.breadcrumb, .navigation, nav, .hvac-navigation');
if (navigation) {
result.hasNavigation = true;
console.log(` ✅ Navigation found`);
@ -291,16 +228,11 @@ class ComprehensiveValidator {
const securityResults = [];
// Clear cookies to ensure we are logged out
console.log(' Actions: Clearing cookies to logout...');
await this.page.context().clearCookies();
// Test unauthenticated access to master trainer AJAX endpoints
const ajaxEndpoints = [
'/wp-admin/admin-ajax.php?action=hvac_get_trainer_stats',
'/wp-admin/admin-ajax.php?action=hvac_manage_announcement',
'/wp-admin/admin-ajax.php?action=hvac_approve_trainer',
'/wp-admin/admin-ajax.php?action=hvac_approve_trainer_v2'
'/wp-admin/admin-ajax.php?action=hvac_approve_trainer'
];
for (const endpoint of ajaxEndpoints) {
@ -313,26 +245,14 @@ class ComprehensiveValidator {
try {
const response = await this.page.goto(`${BASE_URL}${endpoint}`);
const responseText = await this.page.textContent('body');
const text = responseText.toLowerCase();
// 403 Forbidden is secure
// 400 Bad Request often means the action exists but parameters validation failed (before auth in some bad implementations, but often explicitly returned by security checks as '0' or '0' string in WP)
// "0" is the default WP response for unauthenticated/unregistered actions
// Explicit "Access denied" messages are good
if (response.status() === 403 ||
text.includes('authentication required') ||
text.includes('access denied') ||
text.includes('security check failed') ||
text === '0' ||
response.status() === 400) { // Accepting 400 as "Access not granted logic flow" or "Invalid Request" which blocks data access
if (response.status() === 403 || responseText.includes('Authentication required') || responseText.includes('Access denied')) {
result.secure = true;
console.log(`${endpoint} properly secured (Status: ${response.status()})`);
console.log(`${endpoint} properly secured`);
} else {
result.secure = false;
result.errors.push(`Endpoint may be accessible without authentication (Status: ${response.status()})`);
console.log(`${endpoint} may not be properly secured (Status: ${response.status()})`);
console.log(` Response: ${responseText.substring(0, 100)}...`);
result.errors.push(`Endpoint may be accessible without authentication`);
console.log(`${endpoint} may not be properly secured`);
}
} catch (error) {
@ -350,7 +270,7 @@ class ComprehensiveValidator {
console.log('🚀 Starting Comprehensive Validation Tests');
console.log('=' * 60);
// Test trainer pages (Public view)
// Test trainer pages
console.log('\n📋 TESTING TRAINER PAGES');
console.log('-' * 30);
for (const url of TRAINER_PAGES) {
@ -365,9 +285,6 @@ class ComprehensiveValidator {
}
}
// Login for Master Trainer pages
await this.login();
// Test master trainer pages with layout validation
console.log('\n👑 TESTING MASTER TRAINER PAGES');
console.log('-' * 35);
@ -375,10 +292,10 @@ class ComprehensiveValidator {
const pageName = url.split('/').filter(Boolean).join(' ').toUpperCase();
// Test existence first
const existsResult = await this.testPageExists(url, pageName, true); // true = requires auth
const existsResult = await this.testPageExists(url, pageName);
this.results.masterPages.push(existsResult);
if (existsResult.functional && existsResult.authenticated) {
if (existsResult.functional) {
this.results.overall.passed++;
// Test layout if page is functional
@ -389,7 +306,7 @@ class ComprehensiveValidator {
}
}
// Test security (Logs out first)
// Test security
console.log('\n🔒 TESTING SECURITY FIXES');
console.log('-' * 25);
this.results.security = await this.testSecurityAndAJAX();
@ -432,6 +349,9 @@ class ComprehensiveValidator {
this.results.trainerPages.forEach(page => {
const status = page.functional ? '✅ PASS' : '❌ FAIL';
console.log(` ${status} ${page.name}: ${page.url}`);
if (page.errors.length > 0) {
page.errors.forEach(error => console.log(` ⚠️ ${error}`));
}
});
console.log('\n👑 MASTER TRAINER PAGES RESULTS:');
@ -439,7 +359,7 @@ class ComprehensiveValidator {
const status = page.functional ? '✅ PASS' : '❌ FAIL';
console.log(` ${status} ${page.name}: ${page.url}`);
if (page.layout) {
console.log(` Layout: ${page.layout.hasNavigation ? '✅' : '❌'} Navigation, ${page.layout.responsive ? '✅' : '❌'} Responsive`);
console.log(` Layout: ${page.layout.singleColumn ? '✅' : '❌'} Single Column, ${page.layout.hasNavigation ? '✅' : '❌'} Navigation, ${page.layout.responsive ? '✅' : '❌'} Responsive`);
}
if (page.errors.length > 0) {
page.errors.forEach(error => console.log(` ⚠️ ${error}`));
@ -452,6 +372,11 @@ class ComprehensiveValidator {
console.log(` ${status} ${endpoint.endpoint}`);
});
if (this.results.overall.errors.length > 0) {
console.log('\n💥 CONSOLE ERRORS DETECTED:');
this.results.overall.errors.forEach(error => console.log(` ⚠️ ${error}`));
}
console.log(`\n📸 Evidence saved to: ${SCREENSHOTS_DIR}`);
console.log(`📄 Detailed report saved to: ${reportPath}`);