// ===== START OF essenzia-club-core.php =====
/*
Plugin Name: Essenzia Club Core
Description: Affiliate system with commissions, MLM, invoices, and withdrawals.
Version: 3.8.3
Author: Essenzia Tech
Text Domain: essenzia-club-core
Domain Path: /languages
Requires PHP: 7.4
Requires at least: 5.8
WC requires at least: 6.0
WC tested up to: 8.5
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
// Define plugin constants
define('ESSENZIA_CORE_VERSION', '3.8.3');
define('ESSENZIA_CORE_PATH', plugin_dir_path(__FILE__));
define('ESSENZIA_MAIN_PLUGIN_FILE', __FILE__); // NEW: Defines the correct path to this main file
define('ESSENZIA_CORE_URL', plugin_dir_url(__FILE__));
define('ESSENZIA_INC_PATH', ESSENZIA_CORE_PATH . 'inc/');
define('ESSENZIA_TEMPLATES_PATH', ESSENZIA_CORE_PATH . 'templates/');
define('ESSENZIA_ASSETS_URL', ESSENZIA_CORE_URL . 'assets/');
define('ESSENZIA_LIB_PATH', ESSENZIA_CORE_PATH . 'lib/');
define('ESSENZIA_DEFAULT_LOGO_URL', ESSENZIA_ASSETS_URL . 'images/default-logo.png');
define('ESSENZIA_DEFAULT_FONT_PATH', ESSENZIA_ASSETS_URL . 'fonts/DejaVuSans.ttf');
/**
* Main function to initialize the Essenzia Club Core plugin.
*/
function essenzia_club_core_initialize() {
// Re-enabling the dependency check now that the other issue is found.
// If you still have issues, you can comment this block out again.
if (!class_exists('WooCommerce')) {
add_action('admin_notices', 'essenzia_club_core_woocommerce_missing_notice');
return;
}
// Load the plugin's core file.
if (file_exists(ESSENZIA_INC_PATH . 'class-core.php')) {
require_once ESSENZIA_INC_PATH . 'class-core.php';
} else {
add_action('admin_notices', function() {
echo '
';
echo esc_html__('Essenzia Club Core plugin is missing its main class file (inc/class-core.php) and cannot function. Please reinstall the plugin.', 'essenzia-club-core');
echo '
';
echo esc_html__('Essenzia Club Core requires WooCommerce to be installed and active. Please install WooCommerce or deactivate Essenzia Club Core.', 'essenzia-club-core');
echo '
';
}
/**
* Load plugin textdomain for internationalization.
*/
function essenzia_club_core_load_textdomain() {
load_plugin_textdomain(
'essenzia-club-core',
false,
dirname(plugin_basename(__FILE__)) . '/languages/'
);
}
add_action('plugins_loaded', 'essenzia_club_core_load_textdomain', 9);
/**
* Enqueue admin styles.
*/
function essenzia_club_core_enqueue_admin_styles($hook_suffix) {
// Only enqueue styles if WooCommerce is active
if (!class_exists('WooCommerce')) {
return;
}
$main_menu_slug = 'essenzia-dashboard';
$plugin_admin_pages_prefixes = [
'toplevel_page_' . $main_menu_slug,
$main_menu_slug . '_page_',
];
$load_styles = false;
foreach ($plugin_admin_pages_prefixes as $prefix) {
if (strpos($hook_suffix, $prefix) === 0) {
$load_styles = true;
break;
}
}
if ($load_styles) {
wp_enqueue_style(
'essenzia-admin-styles',
ESSENZIA_ASSETS_URL . 'css/essenzia-admin-styles.css',
[],
ESSENZIA_CORE_VERSION
);
}
}
add_action('admin_enqueue_scripts', 'essenzia_club_core_enqueue_admin_styles');
// ===== END OF essenzia-club-core.php =====
// ===== START OF class-core.php =====
/**
* Essenzia Club Core: Main Plugin Loader
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Core {
public static function init() {
// Use the newly defined constant for the main plugin file path
register_activation_hook(ESSENZIA_MAIN_PLUGIN_FILE, [self::class, 'activate_plugin']);
add_action('plugins_loaded', [self::class, 'load_plugin_modules'], 10);
}
public static function activate_plugin() {
$install_class_path = ESSENZIA_INC_PATH . 'class-install.php';
if (!class_exists('Essenzia_Install')) {
if (file_exists($install_class_path)) {
require_once $install_class_path;
} else {
$message = __('Essenzia Club Core critical file missing: class-install.php. Plugin cannot be activated properly. Please ensure all plugin files are uploaded correctly.', 'essenzia-club-core');
error_log($message);
wp_die($message, __('Plugin Activation Error', 'essenzia-club-core'), ['back_link' => true]);
return;
}
}
Essenzia_Install::run_installation();
}
public static function load_plugin_modules() {
$modules = [
'class-install.php', 'functions-utils.php', 'class-users.php',
'class-privacy.php', 'class-admin.php', 'class-dashboard.php',
'class-referrals.php', 'class-commissions.php', 'class-payouts.php',
'class-invoices.php', 'class-emails.php',
];
foreach ($modules as $file) {
$path = ESSENZIA_INC_PATH . $file;
if (file_exists($path)) {
require_once $path;
} else {
// You can add an admin notice for missing files if needed
}
}
// Initialize modules
if (class_exists('Essenzia_Users')) Essenzia_Users::init();
if (class_exists('Essenzia_Admin')) Essenzia_Admin::init();
if (class_exists('Essenzia_Dashboard')) Essenzia_Dashboard::init();
if (class_exists('Essenzia_Referrals')) Essenzia_Referrals::init();
if (class_exists('Essenzia_Commissions')) Essenzia_Commissions::init();
if (class_exists('Essenzia_Payouts')) Essenzia_Payouts::init();
if (class_exists('Essenzia_Invoices')) Essenzia_Invoices::init();
if (class_exists('Essenzia_Emails')) Essenzia_Emails::init();
if (class_exists('Essenzia_Privacy')) Essenzia_Privacy::init();
}
}
// ===== END OF class-core.php =====
// ===== START OF class-admin.php =====
/**
* Essenzia Club Core: Admin Area Management
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Admin {
public static function init() {
error_log('Essenzia Debug Step 8: Essenzia_Admin::init() called. Adding admin_menu action.'); // DEBUG LOG
add_action('admin_menu', [self::class, 'add_admin_menu_pages']);
add_action('admin_action_essenzia_update_user_affiliate_status', [self::class, 'handle_affiliate_status_update']);
}
/**
* Adds menu and submenu pages to the WordPress admin area.
*/
public static function add_admin_menu_pages() {
error_log('Essenzia Debug Step 9: Essenzia_Admin::add_admin_menu_pages() called. Registering menu.'); // DEBUG LOG
add_menu_page(
__('Essenzia Affiliates Dashboard', 'essenzia-club-core'),
__('Essenzia Affiliates', 'essenzia-club-core'),
'manage_options',
'essenzia-dashboard',
[self::class, 'render_dashboard_page'],
'dashicons-groups',
25
);
// ... (rest of the add_submenu_page calls) ...
add_submenu_page(
'essenzia-dashboard',
__('Manage Affiliates', 'essenzia-club-core'),
__('Manage Affiliates', 'essenzia-club-core'),
'manage_users',
'essenzia-manage-affiliates',
[self::class, 'render_manage_affiliates_page']
);
add_submenu_page(
'essenzia-dashboard',
__('Withdrawal Requests', 'essenzia-club-core'),
__('Withdrawals', 'essenzia-club-core'),
'manage_options',
'essenzia-withdrawals',
[self::class, 'render_withdrawals_page']
);
add_submenu_page(
'essenzia-dashboard',
__('Export Logs', 'essenzia-club-core'),
__('Export CSV', 'essenzia-club-core'),
'manage_options',
'essenzia-export',
[self::class, 'render_export_page']
);
add_submenu_page(
'essenzia-dashboard',
__('Affiliate Program Settings', 'essenzia-club-core'),
__('Settings', 'essenzia-club-core'),
'manage_options',
'essenzia-settings',
[self::class, 'render_settings_page']
);
error_log('Essenzia Debug Step 10: Finished registering admin menus.'); // DEBUG LOG
}
// ... (The rest of the Essenzia_Admin class remains the same) ...
public static function render_dashboard_page() {
echo '
' . esc_html__('Essenzia Affiliate System Dashboard', 'essenzia-club-core') . '
';
echo '
' . esc_html__('Welcome! Manage affiliates, withdrawals, settings, and view reports from this central hub.', 'essenzia-club-core') . '
';
echo '
';
}
// ... all other functions like render_withdrawals_page, render_settings_page, etc.
}
// ===== END OF class-admin.php =====
// ===== START OF class-commissions.php =====
/**
* Essenzia Club Core: Commission Calculation and Logging
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Commissions {
public static function init() {
// Hook into WooCommerce order completion to log commissions
add_action('woocommerce_order_status_completed', [self::class, 'log_commissions_on_order_completion'], 10, 1);
}
/**
* Logs commissions for referrers when a WooCommerce order status changes to 'completed'.
*/
public static function log_commissions_on_order_completion($order_id) {
$order = wc_get_order($order_id);
if (!$order) {
error_log(__CLASS__ . " - Order Not Found: Order #{$order_id} could not be retrieved for commission processing.");
return;
}
if (get_post_meta($order_id, '_essenzia_commissions_processed', true)) {
return;
}
$customer_user_id = $order->get_user_id();
$lifetime_commissions_enabled = (get_option('essenzia_lifetime_commissions_enabled', 'yes') === 'yes');
$disallow_own_purchase = (get_option('essenzia_disallow_own_purchase_commission', 'yes') === 'yes');
$commission_on_subtotal_only = (get_option('essenzia_commission_on_subtotal_only', 'no') === 'yes');
$max_levels_setting = max(1, intval(get_option('essenzia_max_affiliate_levels', 3)));
$base_amount = $commission_on_subtotal_only ? $order->get_subtotal() : $order->get_total();
$commissionable_amount = apply_filters('essenzia_commissionable_order_amount', $base_amount, $order);
if ($commissionable_amount <= 0) {
update_post_meta($order_id, '_essenzia_commissions_processed', true);
return;
}
$product_names = [];
foreach ($order->get_items() as $item_id => $item) {
$product_names[] = $item->get_name() . ' (Qty: ' . $item->get_quantity() . ')';
}
$product_list_note = empty($product_names) ? __('N/A', 'essenzia-club-core') : implode('; ', $product_names);
$customer_display_name = 'Guest';
if($customer_user_id) {
$customer_user_data = get_userdata($customer_user_id);
$customer_display_name = $customer_user_data ? $customer_user_data->user_login : '#' . $customer_user_id;
}
$commission_source_note = sprintf(
__('Source: Order #%1$s by customer %2$s. Products: %3$s', 'essenzia-club-core'),
$order_id,
$customer_display_name,
$product_list_note
);
global $wpdb;
$logs_table_name = $wpdb->prefix . 'essenzia_logs';
$commissions_awarded_this_order = false;
if ($lifetime_commissions_enabled) {
if (!$customer_user_id) {
update_post_meta($order_id, '_essenzia_commissions_processed', true);
return;
}
for ($level = 1; $level <= $max_levels_setting; $level++) {
$referrer_user_id = intval(get_user_meta($customer_user_id, 'essenzia_referrer_l' . $level, true));
if ($referrer_user_id <= 0 || !get_userdata($referrer_user_id)) {
continue;
}
// ** NEW CHECK: Referrer must have an active subscription to earn commissions **
if (!essenzia_user_has_active_subscription($referrer_user_id)) { //
error_log("Essenzia Commission Skipped: Referrer ID {$referrer_user_id} does not have an active subscription.");
continue; // Skip to the next level
}
if ($disallow_own_purchase && $referrer_user_id == $customer_user_id) {
continue;
}
$commission_rate = floatval(get_option("essenzia_commission_lvl{$level}", 0));
$commission_cap = floatval(get_option("essenzia_commission_cap", 0));
if ($commission_rate <= 0) {
continue;
}
$calculated_commission = round($commissionable_amount * $commission_rate, 4);
if ($commission_cap > 0 && $calculated_commission > $commission_cap) {
$calculated_commission = $commission_cap;
}
if ($calculated_commission <= 0) {
continue;
}
$log_data = [
'user_id' => $referrer_user_id,
'action_type' => "referral_commission_l{$level}",
'amount' => $calculated_commission,
'note' => $commission_source_note,
'related_id' => $order_id,
'status' => 'approved',
'created_at' => current_time('mysql', 1),
'updated_at' => current_time('mysql', 1),
];
$log_formats = ['%d', '%s', '%f', '%s', '%d', '%s', '%s', '%s'];
$inserted = $wpdb->insert($logs_table_name, $log_data, $log_formats);
if ($inserted) {
$commissions_awarded_this_order = true;
$email_note_for_referrer = sprintf(__('From Order #%s.', 'essenzia-club-core'), $order_id);
do_action('essenzia_notify_new_commission_earned', $referrer_user_id, $calculated_commission, $email_note_for_referrer);
} else {
error_log(__CLASS__ . ": DB Insert Failed for L{$level} commission. User: {$referrer_user_id}, Order: {$order_id}. Error: " . $wpdb->last_error);
}
}
} else {
// Non-Lifetime Commissions Logic (L1 only, based on cookie)
if (isset($_COOKIE['essenzia_affiliate_ref_id'])) {
$referrer_l1_id_from_cookie = intval($_COOKIE['essenzia_affiliate_ref_id']);
if ($referrer_l1_id_from_cookie > 0 && get_userdata($referrer_l1_id_from_cookie)) {
$referrer_user_id = $referrer_l1_id_from_cookie;
if (!essenzia_user_has_active_subscription($referrer_user_id)) {
error_log("Essenzia Commission Skipped: Referrer ID {$referrer_user_id} does not have an active subscription.");
update_post_meta($order_id, '_essenzia_commissions_processed', true);
return;
}
if ($disallow_own_purchase && $customer_user_id && $referrer_user_id == $customer_user_id) {
// Skip commission
} else {
$commission_rate = floatval(get_option("essenzia_commission_lvl1", 0));
$commission_cap = floatval(get_option("essenzia_commission_cap", 0));
if ($commission_rate > 0) {
$calculated_commission = round($commissionable_amount * $commission_rate, 4);
if ($commission_cap > 0 && $calculated_commission > $commission_cap) {
$calculated_commission = $commission_cap;
}
if ($calculated_commission > 0) {
$log_data = [
'user_id' => $referrer_user_id,
'action_type' => "referral_commission_l1_cookie",
'amount' => $calculated_commission,
'note' => $commission_source_note . ' (Non-lifetime via cookie)',
'related_id' => $order_id,
'status' => 'approved',
'created_at' => current_time('mysql', 1),
'updated_at' => current_time('mysql', 1),
];
$log_formats = ['%d', '%s', '%f', '%s', '%d', '%s', '%s', '%s'];
$inserted = $wpdb->insert($logs_table_name, $log_data, $log_formats);
if ($inserted) {
$commissions_awarded_this_order = true;
$email_note_for_referrer = sprintf(__('From Order #%s.', 'essenzia-club-core'), $order_id);
do_action('essenzia_notify_new_commission_earned', $referrer_user_id, $calculated_commission, $email_note_for_referrer);
} else {
error_log(__CLASS__ . ": DB Insert Failed for Non-Lifetime L1 commission. User: {$referrer_user_id}, Order: {$order_id}. Error: " . $wpdb->last_error);
}
}
}
}
}
}
}
update_post_meta($order_id, '_essenzia_commissions_processed', true);
}
}
// ===== END OF class-commissions.php =====
// ===== START OF class-dashboard.php =====
/**
* Essenzia Club Core: Frontend Affiliate Dashboard Management
* Handles shortcodes, asset enqueuing, and template loading for the affiliate dashboard.
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Dashboard {
public static function init() {
// Register shortcodes for frontend affiliate dashboard components
add_shortcode('essenzia_affiliate_dashboard', [self::class, 'render_affiliate_dashboard_shortcode']);
// Individual component shortcodes (can be used within the main dashboard or separately)
add_shortcode('essenzia_affiliate_referral_url_section', [self::class, 'render_referral_url_section_shortcode']);
add_shortcode('essenzia_affiliate_stats_section', [self::class, 'render_stats_section_shortcode']);
add_shortcode('essenzia_affiliate_commissions_log_section', [self::class, 'render_commissions_log_section_shortcode']);
add_shortcode('essenzia_affiliate_withdraw_form_section', [self::class, 'render_withdraw_form_section_shortcode']);
add_shortcode('essenzia_affiliate_withdrawals_log_section', [self::class, 'render_withdrawals_log_section_shortcode']);
add_shortcode('essenzia_affiliate_earnings_history_section', [self::class, 'render_earnings_history_section_shortcode']); // NEW
// Shortcodes for dedicated pages
add_shortcode('essenzia_affiliate_login_page', [self::class, 'render_login_page_shortcode']);
add_shortcode('essenzia_affiliate_register_page', [self::class, 'render_register_page_shortcode']);
add_shortcode('essenzia_affiliate_invoices_page', [self::class, 'render_invoices_page_shortcode']);
add_shortcode('essenzia_affiliate_leaderboard_page', [self::class, 'render_leaderboard_page_shortcode']);
add_shortcode('essenzia_affiliate_payout_request_page', [self::class, 'render_payout_request_page_shortcode']);
// Enqueue frontend scripts and styles if shortcodes are present on the page
add_action('wp_enqueue_scripts', [self::class, 'enqueue_frontend_assets']);
// AJAX handler for CSV export from user dashboard
add_action('wp_ajax_essenzia_export_user_earnings_csv', [self::class, 'handle_user_earnings_csv_export']);
}
/**
* Enqueue scripts and styles for the frontend dashboard.
* Only loads if one of the plugin's shortcodes is detected on the current page.
*/
public static function enqueue_frontend_assets() {
global $post;
$load_assets = false;
$shortcodes_to_check = [
'essenzia_affiliate_dashboard',
'essenzia_affiliate_referral_url_section', 'essenzia_affiliate_stats_section',
'essenzia_affiliate_commissions_log_section', 'essenzia_affiliate_withdraw_form_section',
'essenzia_affiliate_withdrawals_log_section', 'essenzia_affiliate_earnings_history_section', // NEW
'essenzia_affiliate_login_page', 'essenzia_affiliate_register_page',
'essenzia_affiliate_invoices_page', 'essenzia_affiliate_leaderboard_page',
'essenzia_affiliate_payout_request_page'
];
if (is_a($post, 'WP_Post') && $post->post_content) {
foreach ($shortcodes_to_check as $sc) {
if (has_shortcode($post->post_content, $sc)) {
$load_assets = true;
break;
}
}
}
// Allow themes/plugins to force load assets if shortcode detection fails (e.g. in widgets or page builders)
if (apply_filters('essenzia_force_load_frontend_assets', false)) {
$load_assets = true;
}
if ($load_assets) {
wp_enqueue_style(
'essenzia-frontend-styles',
ESSENZIA_ASSETS_URL . 'css/essenzia-club-styles.css', //
[],
ESSENZIA_CORE_VERSION
);
wp_enqueue_script(
'essenzia-frontend-scripts',
ESSENZIA_ASSETS_URL . 'js/essenzia-frontend-scripts.js', //
['jquery'], // Add jQuery as a dependency if needed by the script
ESSENZIA_CORE_VERSION,
true // Load in footer
);
// Localize script with data needed by frontend JS
wp_localize_script('essenzia-frontend-scripts', 'essenziaDashboardData', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('essenzia_frontend_ajax_nonce'),
'i18n' => [ // Internationalization strings for JS
'copied' => esc_html__('Copied!', 'essenzia-club-core'),
'copy_failed' => esc_html__('Failed to copy. Please select and copy manually.', 'essenzia-club-core'),
'confirmExport' => esc_html__('Are you sure you want to export your earnings history?', 'essenzia-club-core'),
]
]);
}
}
// --- Main Dashboard Shortcode --- //
public static function render_affiliate_dashboard_shortcode($atts = [], $content = null) {
// Ensure user is logged in and approved, or show appropriate message.
// This logic is primarily handled within the 'dashboard/main.php' template itself.
return self::render_template_part('dashboard/main.php');
}
// --- Individual Dashboard Section Shortcodes --- //
public static function render_referral_url_section_shortcode($atts = [], $content = null) {
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
return self::render_template_part('dashboard/section-referral-url.php', ['user_id' => get_current_user_id()]);
}
public static function render_stats_section_shortcode($atts = [], $content = null) {
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
$user_id = get_current_user_id();
$template_args = [
'user_id' => $user_id,
'current_balance' => essenzia_get_affiliate_balance($user_id),
'total_referrals' => essenzia_get_total_referrals_count($user_id),
'total_clicks' => essenzia_get_total_clicks_count($user_id), // Stubbed for now
'conversion_rate' => essenzia_get_conversion_rate($user_id), // Depends on clicks
'total_paid_earnings' => essenzia_get_total_paid_earnings($user_id),
// Placeholder for chart data - this would typically be an array of labels and values
// 'chart_data' => json_encode(['labels' => ['Jan', 'Feb', 'Mar'], 'data' => [10, 20, 15]]),
];
return self::render_template_part('dashboard/section-stats-overview.php', $template_args); //
}
public static function render_commissions_log_section_shortcode($atts = [], $content = null) {
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
$default_atts = ['limit' => 5, 'show_pagination' => 'no']; // Default attributes
$merged_atts = shortcode_atts($default_atts, $atts, 'essenzia_affiliate_commissions_log_section');
return self::render_template_part('dashboard/section-commissions-log.php', [
'user_id' => get_current_user_id(),
'limit' => intval($merged_atts['limit']),
'show_pagination' => filter_var($merged_atts['show_pagination'], FILTER_VALIDATE_BOOLEAN),
]);
}
public static function render_earnings_history_section_shortcode($atts = [], $content = null) { // NEW
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
$default_atts = ['limit' => 10, 'show_pagination' => 'yes'];
$merged_atts = shortcode_atts($default_atts, $atts, 'essenzia_affiliate_earnings_history_section');
return self::render_template_part('dashboard/section-earnings-history.php', [ //
'user_id' => get_current_user_id(),
'limit' => intval($merged_atts['limit']),
'show_pagination' => filter_var($merged_atts['show_pagination'], FILTER_VALIDATE_BOOLEAN),
]);
}
public static function render_withdraw_form_section_shortcode($atts = [], $content = null) {
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
$user_id = get_current_user_id();
$template_args = [
'user_id' => $user_id,
'min_withdrawal_amount' => floatval(get_option('essenzia_min_withdrawal_amount', 20)),
'current_balance' => essenzia_get_affiliate_balance($user_id),
'has_pending_withdrawal' => class_exists('Essenzia_Payouts') ? Essenzia_Payouts::has_pending_withdrawal_request($user_id) : false,
'feedback_message' => isset($_GET['withdraw_status']) || isset($_GET['withdraw_error']) ? self::get_withdrawal_feedback_message() : '', //
'message_type' => isset($_GET['withdraw_status']) && $_GET['withdraw_status'] === 'success' ? 'success' : (isset($_GET['withdraw_error']) ? 'error' : ''),
'nonce_action' => 'essenzia_request_withdrawal_action',
'nonce_name' => 'essenzia_withdraw_nonce',
];
return self::render_template_part('dashboard/section-withdraw-form.php', $template_args);
}
public static function render_withdrawals_log_section_shortcode($atts = [], $content = null) {
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
$default_atts = ['limit' => 5, 'show_pagination' => 'no'];
$merged_atts = shortcode_atts($default_atts, $atts, 'essenzia_affiliate_withdrawals_log_section');
return self::render_template_part('dashboard/section-withdrawals-log.php', [
'user_id' => get_current_user_id(),
'limit' => intval($merged_atts['limit']),
'show_pagination' => filter_var($merged_atts['show_pagination'], FILTER_VALIDATE_BOOLEAN),
]);
}
// --- Dedicated Page Shortcodes --- //
public static function render_login_page_shortcode($atts = [], $content = null) {
return self::render_template_part('page-affiliate-login.php');
}
public static function render_register_page_shortcode($atts = [], $content = null) {
return self::render_template_part('page-affiliate-register.php');
}
public static function render_invoices_page_shortcode($atts = [], $content = null) {
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
return self::render_template_part('page-affiliate-invoices.php', ['user_id' => get_current_user_id()]);
}
public static function render_leaderboard_page_shortcode($atts = [], $content = null) {
return self::render_template_part('page-affiliate-leaderboard.php');
}
public static function render_payout_request_page_shortcode($atts = [], $content = null) {
if (!is_user_logged_in()) return self::render_template_part('global/login-required.php');
$user_id = get_current_user_id();
$template_args = [ // Same args as withdraw_form_section
'user_id' => $user_id,
'min_withdrawal_amount' => floatval(get_option('essenzia_min_withdrawal_amount', 20)),
'current_balance' => essenzia_get_affiliate_balance($user_id),
'has_pending_withdrawal' => class_exists('Essenzia_Payouts') ? Essenzia_Payouts::has_pending_withdrawal_request($user_id) : false,
'feedback_message' => isset($_GET['withdraw_status']) || isset($_GET['withdraw_error']) ? self::get_withdrawal_feedback_message() : '',
'message_type' => isset($_GET['withdraw_status']) && $_GET['withdraw_status'] === 'success' ? 'success' : (isset($_GET['withdraw_error']) ? 'error' : ''),
'nonce_action' => 'essenzia_request_withdrawal_action',
'nonce_name' => 'essenzia_withdraw_nonce',
];
return self::render_template_part('page-affiliate-payout-request.php', $template_args);
}
/**
* Helper to get withdrawal feedback messages based on query args.
* Public static so it can be called from templates if needed (as in page-affiliate-payout-request.php).
* @return string The feedback message.
*/
public static function get_withdrawal_feedback_message() { //
if (isset($_GET['withdraw_status'])) {
if ($_GET['withdraw_status'] === 'success') {
return __('Your withdrawal request has been submitted successfully and is pending review.', 'essenzia-club-core');
} elseif ($_GET['withdraw_status'] === 'failed') {
// Consider adding a more specific error code here if available from the Payouts class
return __('There was an error submitting your withdrawal request. Please try again or contact support.', 'essenzia-club-core');
}
} elseif (isset($_GET['withdraw_error'])) {
$min_req_val = floatval(get_option('essenzia_min_withdrawal_amount', 20));
$min_req_formatted = function_exists('essenzia_format_money') ? essenzia_format_money($min_req_val) : '$' . number_format($min_req_val, 2);
switch ($_GET['withdraw_error']) {
case 'security_check_failed': return __('Security check failed. Please try again.', 'essenzia-club-core');
case 'invalid_amount': return __('Invalid withdrawal amount. Please enter a positive number.', 'essenzia-club-core');
case 'min_amount_not_met':
$min_from_url = isset($_GET['min']) ? floatval($_GET['min']) : $min_req_val;
$min_formatted = function_exists('essenzia_format_money') ? essenzia_format_money($min_from_url) : '$' . number_format($min_from_url, 2);
return sprintf(__('Minimum withdrawal amount is %s.', 'essenzia-club-core'), $min_formatted);
case 'insufficient_funds':
$bal_from_url = isset($_GET['balance']) ? floatval($_GET['balance']) : (is_user_logged_in() ? essenzia_get_affiliate_balance(get_current_user_id()) : 0);
$bal_formatted = function_exists('essenzia_format_money') ? essenzia_format_money($bal_from_url) : '$' . number_format($bal_from_url, 2);
return sprintf(__('Insufficient funds. Your current available balance is %s.', 'essenzia-club-core'), $bal_formatted);
case 'pending_exists': return __('You already have a withdrawal request pending. Please wait for it to be processed.', 'essenzia-club-core');
default: return __('An unknown error occurred with your withdrawal request.', 'essenzia-club-core');
}
}
return '';
}
/**
* Renders a template part from the 'templates' directory.
* Extracts $args into individual variables for use within the template.
*
* @param string $template_file_path Relative path to the template file from ESSENZIA_TEMPLATES_PATH.
* @param array $args Optional. Associative array of arguments to pass to the template.
* @return string The rendered HTML output from the template.
*/
public static function render_template_part($template_file_path, $args = []) {
$full_template_path = ESSENZIA_TEMPLATES_PATH . ltrim($template_file_path, '/');
if (!file_exists($full_template_path)) {
$error_msg = sprintf(esc_html__('Error: Essenzia template file missing: %s', 'essenzia-club-core'), esc_html($template_file_path));
error_log("Essenzia Dashboard: Template not found - {$full_template_path}");
// Display error to admin only for debugging purposes
return current_user_can('manage_options') ? '
' . $error_msg . '
' : '';
}
ob_start();
if (is_array($args) && !empty($args)) {
extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
}
include $full_template_path;
return ob_get_clean();
}
/**
* Handles AJAX request for exporting user earnings CSV.
*/
public static function handle_user_earnings_csv_export() {
check_ajax_referer('essenzia_frontend_ajax_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error(['message' => __('User not logged in.', 'essenzia-club-core')], 403);
return;
}
$user_id = get_current_user_id();
global $wpdb;
$logs_table_name = $wpdb->prefix . 'essenzia_logs';
// Fetch all logs for the user (commissions and withdrawals)
$logs = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$logs_table_name} WHERE user_id = %d ORDER BY created_at DESC", $user_id
), ARRAY_A);
if (empty($logs)) {
// Though the button might be hidden if no logs, handle this case.
// This response won't trigger a download but can be handled by JS if needed.
wp_send_json_error(['message' => __('No earnings history found to export.', 'essenzia-club-core')], 404);
return;
}
$filename = sanitize_file_name('essenzia_my_earnings_' . $user_id . '_' . date('Y-m-d') . '.csv');
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Pragma: no-cache');
header('Expires: 0');
$output_stream = fopen('php://output', 'w');
if (!$output_stream) {
wp_send_json_error(['message' => __('Failed to open output stream for CSV export.', 'essenzia-club-core')], 500);
return;
}
fprintf($output_stream, chr(0xEF).chr(0xBB).chr(0xBF)); // BOM for Excel
fputcsv($output_stream, [
__('Date', 'essenzia-club-core'),
__('Type', 'essenzia-club-core'),
__('Description/Note', 'essenzia-club-core'),
__('Amount', 'essenzia-club-core'),
__('Related ID', 'essenzia-club-core'),
__('Status', 'essenzia-club-core'),
]);
foreach ($logs as $log_row) {
fputcsv($output_stream, [
essenzia_format_date($log_row['created_at']),
ucwords(str_replace(['_', 'referral', 'commission', 'l'], ['', '', '', 'L'], $log_row['action_type'])), // More friendly type
$log_row['note'],
essenzia_format_money($log_row['amount']),
$log_row['related_id'],
ucfirst($log_row['status']),
]);
}
fclose($output_stream);
exit;
}
}
// ===== END OF class-dashboard.php =====
// ===== START OF class-emails.php =====
/**
* Essenzia Club Core: Email Notifications
* Handles sending various email notifications to users and admins.
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Emails {
public static function init() {
// Hook for sending email when a new commission is earned
add_action('essenzia_notify_new_commission_earned', [self::class, 'trigger_new_commission_email_to_user'], 10, 3);
// Hook for sending email to admin when a new withdrawal request is submitted
add_action('essenzia_notify_admin_new_withdrawal_request', [self::class, 'trigger_new_withdrawal_request_email_to_admin'], 10, 3);
// Hook for sending email to user when their withdrawal request is processed (paid or rejected)
add_action('essenzia_notify_user_withdrawal_processed', [self::class, 'trigger_withdrawal_processed_email_to_user'], 10, 4);
// Hook for affiliate status change notifications (e.g., approved, rejected)
add_action('essenzia_notify_user_affiliate_status_change', [self::class, 'trigger_affiliate_status_change_email'], 10, 3);
}
/**
* Triggers and sends an email to the user upon earning a new commission.
*
* @param int $recipient_user_id The ID of the user who earned the commission.
* @param float $commission_amount The amount of commission earned.
* @param string $commission_details Optional. Additional details about the commission source.
*/
public static function trigger_new_commission_email_to_user($recipient_user_id, $commission_amount, $commission_details = '') {
$user = get_userdata($recipient_user_id);
if (!$user || !is_email($user->user_email)) {
error_log(__CLASS__ . ": Invalid user or email for new commission notification (User ID: {$recipient_user_id}).");
return;
}
$to_email = $user->user_email;
/* translators: %s: Site Name */
$subject = sprintf(esc_html__('🎉 New Commission Earned at %s!', 'essenzia-club-core'), get_bloginfo('name'));
$formatted_amount = essenzia_format_money($commission_amount);
$dashboard_url = function_exists('essenzia_get_affiliate_dashboard_url') ? essenzia_get_affiliate_dashboard_url() : home_url('/affiliate-dashboard/');
$email_body_content = "
" . esc_html__('🎉 Congratulations! You have earned a new commission.', 'essenzia-club-core') . "
" . sprintf(
wp_kses_post(__('You can view your earnings and full history in your affiliate dashboard.', 'essenzia-club-core')), //
esc_url($dashboard_url)
) . "
" .
"
" . esc_html__('Thank you for being a valued Essenzia partner!', 'essenzia-club-core') . "
";
$html_message = self::get_html_email_template($subject, $email_body_content);
$headers = self::get_standard_email_headers();
wp_mail($to_email, $subject, $html_message, $headers);
}
/**
* Triggers and sends an email to the admin when a user submits a new withdrawal request.
*
* @param int $requesting_user_id The ID of the user who submitted the request.
* @param float $withdrawal_amount The amount requested for withdrawal.
* @param string $user_provided_note Optional. Note provided by the user with their request.
*/
public static function trigger_new_withdrawal_request_email_to_admin($requesting_user_id, $withdrawal_amount, $user_provided_note = '') {
$admin_email = get_option('admin_email');
if (!is_email($admin_email)) {
error_log(__CLASS__ . ": Admin email not configured or invalid for new withdrawal request notification.");
return;
}
$user = get_userdata($requesting_user_id);
if (!$user) {
error_log(__CLASS__ . ": Invalid user for new withdrawal request notification (User ID: {$requesting_user_id}).");
return;
}
/* translators: %s: Site Name */
$subject = sprintf(esc_html__('🔔 New Withdrawal Request on %s', 'essenzia-club-core'), get_bloginfo('name'));
$formatted_amount = essenzia_format_money($withdrawal_amount);
$admin_withdrawals_url = admin_url('admin.php?page=essenzia-withdrawals');
$email_body_content = "
" . esc_html__('A New Withdrawal Request Has Been Submitted', 'essenzia-club-core') . "
" .
"
" . sprintf(
/* translators: 1: User Display Name, 2: User ID, 3: User Email */
esc_html__('User: %1$s (ID: %2$d, Email: %3$s)', 'essenzia-club-core'),
esc_html($user->display_name), $requesting_user_id, esc_html($user->user_email)
) . "
" . sprintf(
wp_kses_post(__('Please review this request in the admin panel (Withdrawals section).', 'essenzia-club-core')),
esc_url($admin_withdrawals_url)
) . "
";
$html_message = self::get_html_email_template($subject, $email_body_content);
$headers = self::get_standard_email_headers();
wp_mail($admin_email, $subject, $html_message, $headers);
}
/**
* Triggers and sends an email to the user when their withdrawal request is processed (paid or rejected).
*
* @param int $recipient_user_id The ID of the user whose request was processed.
* @param float $processed_amount The amount that was processed.
* @param string $status The new status of the request ('paid' or 'rejected').
* @param string $admin_note Optional. A note from the admin regarding the processing.
*/
public static function trigger_withdrawal_processed_email_to_user($recipient_user_id, $processed_amount, $status, $admin_note = '') {
$user = get_userdata($recipient_user_id);
if (!$user || !is_email($user->user_email)) {
error_log(__CLASS__ . ": Invalid user or email for withdrawal processed notification (User ID: {$recipient_user_id}).");
return;
}
$to_email = $user->user_email;
$formatted_amount = essenzia_format_money($processed_amount);
$subject = '';
$email_body_content = '';
$dashboard_url = function_exists('essenzia_get_affiliate_dashboard_url') ? essenzia_get_affiliate_dashboard_url() : home_url('/affiliate-dashboard/');
if ($status === 'paid') {
/* translators: %s: Site Name */
$subject = sprintf(esc_html__('💸 Your Withdrawal Has Been Processed at %s!', 'essenzia-club-core'), get_bloginfo('name'));
$email_body_content = "
" . esc_html__('Your Withdrawal Request Has Been Paid!', 'essenzia-club-core') . "
" . sprintf(esc_html__('Your withdrawal request for %s has been processed and marked as paid.', 'essenzia-club-core'), "" . $formatted_amount . "") . "
";
} elseif ($status === Essenzia_Users::STATUS_REJECTED) { // Using constant for clarity
/* translators: %s: Site Name */
$subject = sprintf(esc_html__('ℹ️ Update on Your Withdrawal Request at %s', 'essenzia-club-core'), get_bloginfo('name'));
$email_body_content = "
" . esc_html__('Update on Your Withdrawal Request', 'essenzia-club-core') . "
" . sprintf(esc_html__('Your withdrawal request for %s has unfortunately been rejected.', 'essenzia-club-core'), "" . $formatted_amount . "") . "
";
} else {
error_log(__CLASS__ . ": Invalid status '{$status}' for withdrawal processed notification (User ID: {$recipient_user_id}).");
return; // Do not send email for unknown status
}
if (!empty($admin_note)) {
$email_body_content .= "
" . sprintf(
wp_kses_post(__('You can check the status and details in your affiliate dashboard.', 'essenzia-club-core')),
esc_url($dashboard_url)
) . "
" .
"
" . esc_html__('If you have any questions, please contact support.', 'essenzia-club-core') . "
";
$html_message = self::get_html_email_template($subject, $email_body_content);
$headers = self::get_standard_email_headers();
wp_mail($to_email, $subject, $html_message, $headers);
}
/**
* Triggers and sends an email to the user when their affiliate status changes (e.g., approved, rejected).
*
* @param int $user_id The ID of the user whose status changed.
* @param string $new_status The new affiliate status (e.g., Essenzia_Users::STATUS_APPROVED).
* @param string $old_status The previous affiliate status (can be empty if new user).
*/
public static function trigger_affiliate_status_change_email($user_id, $new_status, $old_status) {
$user = get_userdata($user_id);
if (!$user || !is_email($user->user_email)) {
error_log(__CLASS__ . ": Invalid user or email for affiliate status change notification (User ID: {$user_id}).");
return;
}
// Avoid sending email if status hasn't meaningfully changed for notification purposes.
// Or if it's just the initial 'pending' status set upon registration (unless a specific "pending review" email is desired here).
if ($new_status === $old_status || ($new_status === Essenzia_Users::STATUS_PENDING && empty($old_status))) {
return;
}
$to_email = $user->user_email;
$site_name = get_bloginfo('name');
$subject = '';
$email_body_content = '';
$dashboard_url = function_exists('essenzia_get_affiliate_dashboard_url') ? essenzia_get_affiliate_dashboard_url() : home_url('/affiliate-dashboard/');
if ($new_status === Essenzia_Users::STATUS_APPROVED) {
/* translators: %s: Site Name */
$subject = sprintf(__('✅ Your Affiliate Account at %s is Now Approved!', 'essenzia-club-core'), $site_name);
$email_body_content = "
" . __('Congratulations! Your affiliate account with us has been approved.', 'essenzia-club-core') . "
" .
"
" . sprintf(
wp_kses_post(__('You can now log in to your affiliate dashboard to get your referral link and start promoting.', 'essenzia-club-core')),
esc_url($dashboard_url)
) . "
";
} elseif ($new_status === Essenzia_Users::STATUS_REJECTED) {
/* translators: %s: Site Name */
$subject = sprintf(__('Affiliate Account Status Update from %s', 'essenzia-club-core'), $site_name);
$email_body_content = "
" . __('We are writing to inform you about an update to your affiliate account application. After careful review, we regret to inform you that your application has not been approved at this time.', 'essenzia-club-core') . "
" .
"
" . __('If you have any questions or would like more information, please feel free to contact our support team.', 'essenzia-club-core') . "
";
} else {
// Optionally handle other status changes if needed (e.g., from approved to pending/inactive).
// For now, we only notify on transitions to Approved or Rejected from other states.
return;
}
$email_body_content .= "
'; // End main container & outer table
return $html;
}
/**
* Returns standard email headers for HTML emails.
* Sets 'Content-Type' and a 'From' address.
*
* @return array Array of email header strings.
*/
private static function get_standard_email_headers() {
$site_name = get_bloginfo('name');
// Generate a "no-reply" email from the site's domain
$from_domain = preg_replace('/^www\./i', '', wp_parse_url(home_url(), PHP_URL_HOST));
$from_email = 'no-reply@' . $from_domain;
$headers = [
'Content-Type: text/html; charset=UTF-8',
'From: ' . esc_html($site_name) . ' <' . sanitize_email($from_email) . '>',
];
return $headers;
}
}
// ===== END OF class-emails.php =====
// ===== START OF class-install.php =====
/**
* Essenzia Club Core: Installation and Activation
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Install {
/**
* Runs the installation tasks: creates/updates database tables and sets/updates default options.
* This is called on plugin activation.
*/
public static function run_installation() {
global $wpdb;
$table_name = $wpdb->prefix . 'essenzia_logs';
$charset_collate = $wpdb->get_charset_collate();
// SQL to create/update the logs table. dbDelta handles updates if structure changes.
// Increased precision for amount (DECIMAL(15,4)) to handle smaller commission rates better.
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
action_type VARCHAR(50) NOT NULL,
amount DECIMAL(15,4) DEFAULT 0.0000, /* Increased precision for smaller commission rates */
note TEXT,
related_id BIGINT UNSIGNED DEFAULT NULL, /* e.g., order_id, post_id */
status VARCHAR(20) DEFAULT 'pending', /* e.g., 'pending', 'approved', 'paid', 'rejected' */
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_user_id_action_type (user_id, action_type), /* Index for filtering by user and action */
KEY idx_status_action_type (status, action_type), /* Index for filtering by status and action */
KEY idx_created_at (created_at) /* Index for sorting by date */
) {$charset_collate};";
// We need to load the upgrade file to use dbDelta()
if (!function_exists('dbDelta')) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
}
dbDelta($sql); // dbDelta will handle table creation and updates if structure changes
// Set or update default plugin options
self::set_default_options();
}
/**
* Sets default plugin options.
* Uses add_option which only adds the option if it doesn't already exist.
*/
public static function set_default_options() {
$default_options = [
// Affiliate Structure
'essenzia_max_affiliate_levels' => 3, // Default to 3 levels
'essenzia_max_direct_referrals' => 10, // NEW: Max direct referrals for L1 (user requirement)
// Commission Rates - Adjusted to user requirements
'essenzia_commission_lvl1' => 0.01, // 1% (User Requirement)
'essenzia_commission_lvl2' => 0.005, // 0.5% (User Requirement)
'essenzia_commission_lvl3' => 0.0025, // 0.25% (User Requirement)
// Initialize rates for potential levels up to 10, admin can adjust
'essenzia_commission_lvl4' => 0,
'essenzia_commission_lvl5' => 0,
'essenzia_commission_lvl6' => 0,
'essenzia_commission_lvl7' => 0,
'essenzia_commission_lvl8' => 0,
'essenzia_commission_lvl9' => 0,
'essenzia_commission_lvl10' => 0,
// Commission Rules
'essenzia_commission_cap' => 0, // 0 for no cap by default
'essenzia_lifetime_commissions_enabled' => 'yes',
'essenzia_disallow_own_purchase_commission' => 'yes',
'essenzia_commission_on_subtotal_only' => 'no', // Default: calculate on order total
// Referral & Payout
'essenzia_cookie_validity_days' => 30, // Default: 30 days cookie validity
'essenzia_min_withdrawal_amount' => 20, // Default minimum withdrawal
'essenzia_manual_approval_enabled' => 'yes',// Default: Manual approval for new affiliates required
// Company details for invoices (defaults can be empty or site info)
'essenzia_company_name' => get_bloginfo('name'),
'essenzia_logo_url_option_name' => defined('ESSENZIA_DEFAULT_LOGO_URL') ? ESSENZIA_DEFAULT_LOGO_URL : '',
'essenzia_company_address_1' => '',
'essenzia_company_address_2' => '',
'essenzia_company_city_state_zip' => '',
'essenzia_company_country' => '',
'essenzia_company_vat_id' => '',
// Page Slugs (for URL generation, admin can update these if pages are different)
// These are not directly settings in the UI but good to have defaults if functions rely on options.
// For now, these are hardcoded in functions-utils.php but could be options.
// 'essenzia_dashboard_page_slug' => 'affiliate-dashboard',
// 'essenzia_login_page_slug' => 'affiliate-login',
// 'essenzia_register_page_slug' => 'affiliate-register',
// Placeholder for other features that might need options
// 'essenzia_enable_leaderboard' => 'yes', // Example
];
foreach ($default_options as $option_name => $default_value) {
add_option($option_name, $default_value); // add_option does not update if option already exists
}
// Set a version option to track plugin version for future upgrades/migrations if needed
update_option('essenzia_core_version', ESSENZIA_CORE_VERSION);
}
}
// ===== END OF class-install.php =====
// ===== START OF class-invoices.php =====
/**
* Essenzia Club Core: Invoice Generation
* Handles PDF invoice generation for paid withdrawals using Dompdf.
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
// --- Dompdf Library Integration ---
// Option 1: Composer (Recommended)
// If your plugin uses Composer, add `dompdf/dompdf` to your `composer.json`:
// `composer require dompdf/dompdf`
// Then include Composer's autoloader: `require_once ESSENZIA_CORE_PATH . 'vendor/autoload.php';`
// Option 2: Manual Inclusion
// Download Dompdf and place it in the `lib/dompdf` directory.
// The autoloader below will attempt to load it.
$dompdf_autoloader = ESSENZIA_LIB_PATH . 'dompdf/autoload.inc.php'; //
if (file_exists($dompdf_autoloader)) {
require_once $dompdf_autoloader;
}
// --- End Dompdf Library Integration ---
use Dompdf\Dompdf;
use Dompdf\Options as DompdfOptions; // Aliased to avoid conflict if WP has an Options class.
class Essenzia_Invoices {
private static $dompdf_loaded = false;
public static function init() {
self::$dompdf_loaded = class_exists('Dompdf\Dompdf'); //
if (!self::$dompdf_loaded) {
add_action('admin_notices', [self::class, 'dompdf_missing_admin_notice']);
error_log('Essenzia Club Core: Dompdf library is missing or not loaded. PDF invoice generation will be disabled.');
return; // Do not add further hooks if Dompdf is not available
}
// Action to check if an invoice download is requested via GET parameter
// Runs early, suitable for file downloads that need to set headers.
add_action('template_redirect', [self::class, 'check_for_invoice_download_request'], 5); //Priority 5 to run fairly early
}
/**
* Displays an admin notice if the Dompdf library is missing.
*/
public static function dompdf_missing_admin_notice() {
if (current_user_can('manage_options')) {
echo '
';
echo wp_kses_post(
sprintf(
/* translators: 1: Strong tag start, 2: Strong tag end, 3: Plugin name, 4: Link to Dompdf GitHub. */
__('%1$sEssenzia Club Core Notice:%2$s The Dompdf library is required for PDF invoice generation but was not found. Please install it via Composer (%3$s) or place it in the %4$s directory. Invoice generation is currently disabled.', 'essenzia-club-core'),
'',
'',
'composer require dompdf/dompdf',
'wp-content/plugins/' . basename(ESSENZIA_CORE_PATH) . '/lib/dompdf/'
)
);
echo '
';
}
}
/**
* Checks for 'essenzia_action=download_invoice' GET parameter and triggers PDF generation.
*/
public static function check_for_invoice_download_request() {
if (!self::$dompdf_loaded) return; // Feature disabled
if (!is_user_logged_in()) {
// Though direct access to this URL is unlikely without being logged in (due to nonce from dashboard),
// this check adds an extra layer.
return;
}
// Check for the specific action, log_id, and nonce
if (isset($_GET['essenzia_action']) && $_GET['essenzia_action'] === 'download_invoice' &&
isset($_GET['log_id']) && is_numeric($_GET['log_id']) &&
isset($_GET['_wpnonce'])) {
$log_id = intval($_GET['log_id']);
$nonce = sanitize_text_field(wp_unslash($_GET['_wpnonce']));
// Verify nonce for security. Nonce is specific to the log_id.
if (!wp_verify_nonce($nonce, 'essenzia_generate_invoice_nonce_' . $log_id)) {
wp_die(
esc_html__('Security check failed for invoice download. The link may have expired or is invalid. Please try again from your dashboard.', 'essenzia-club-core'),
esc_html__('Invalid Nonce', 'essenzia-club-core'),
['response' => 403, 'back_link' => true]
);
}
self::generate_and_stream_invoice_pdf($log_id);
// generate_and_stream_invoice_pdf() will call exit() after streaming PDF
}
}
/**
* Generates a URL for downloading an invoice for a specific log entry (typically a paid withdrawal).
*
* @param int $log_id The ID of the log entry.
* @return string The download URL, or '#' if conditions not met (e.g., Dompdf not loaded).
*/
public static function get_invoice_download_url($log_id) {
if (!self::$dompdf_loaded || !is_user_logged_in()) {
return '#'; // Or a login URL or error message URL
}
$log_id = intval($log_id);
// Create a nonce specific to this action and log ID for enhanced security
$nonce = wp_create_nonce('essenzia_generate_invoice_nonce_' . $log_id);
// Use home_url() or site_url(). Add query args for our action.
return add_query_arg([
'essenzia_action' => 'download_invoice',
'log_id' => $log_id,
'_wpnonce' => $nonce
], home_url('/')); // Using home_url('/') is generally fine as 'init' or 'template_redirect' hook is early.
}
/**
* Generates and streams an invoice PDF for a given log ID.
* Invoices are typically for 'paid' 'withdraw_request' logs.
*
* @param int $log_id The ID of the log entry to generate an invoice for.
*/
public static function generate_and_stream_invoice_pdf($log_id) {
if (!self::$dompdf_loaded) {
wp_die(
esc_html__('PDF generation is currently unavailable. The required PDF library (Dompdf) is missing or not configured correctly. Please contact the site administrator.', 'essenzia-club-core'),
esc_html__('PDF Library Error', 'essenzia-club-core'),
['response' => 500, 'back_link' => true]
);
}
global $wpdb;
$current_user_id = get_current_user_id();
$logs_table = $wpdb->prefix . 'essenzia_logs';
// Fetch the log entry. User must own this log entry, and it must be a 'paid' 'withdraw_request'.
$log_entry = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$logs_table} WHERE id = %d AND user_id = %d AND action_type = 'withdraw_request' AND status = 'paid'",
$log_id,
$current_user_id
));
if (!$log_entry) {
wp_die(
esc_html__('Invoice not found, or you do not have permission to view this invoice. Invoices are typically available for paid withdrawal requests only.', 'essenzia-club-core'),
esc_html__('Invoice Access Error', 'essenzia-club-core'),
['response' => 404, 'back_link' => true]
);
}
$user_data = get_userdata($current_user_id);
if (!$user_data) {
wp_die(
esc_html__('User data could not be retrieved for invoice generation.', 'essenzia-club-core'),
esc_html__('User Data Error', 'essenzia-club-core'),
['response' => 500, 'back_link' => true]
);
}
// Prepare variables for the template
$template_args = [
'log_entry' => $log_entry,
'user_data' => $user_data,
'invoice_title' => sprintf(__('Payout Invoice #%s', 'essenzia-club-core'), $log_entry->id),
'company_name' => get_option('essenzia_company_name', get_bloginfo('name')),
'company_logo_url' => get_option('essenzia_logo_url_option_name', (defined('ESSENZIA_DEFAULT_LOGO_URL') ? ESSENZIA_DEFAULT_LOGO_URL : '')),
'company_address_1' => get_option('essenzia_company_address_1', ''),
'company_address_2' => get_option('essenzia_company_address_2', ''),
'company_city_state_zip' => get_option('essenzia_company_city_state_zip', ''),
'company_country' => get_option('essenzia_company_country', ''),
'company_vat_id' => get_option('essenzia_company_vat_id', ''),
];
// Get HTML content from the template file
// Ensure Essenzia_Dashboard class is loaded if render_template_part is used this way
if (!class_exists('Essenzia_Dashboard')) {
wp_die(esc_html__('Core dashboard class not loaded, cannot render invoice template.', 'essenzia-club-core'));
}
$html_content = Essenzia_Dashboard::render_template_part('invoice/pdf-template.php', $template_args);
if (empty(trim($html_content)) || strpos($html_content, 'Template file missing') !== false) {
wp_die(
esc_html__('PDF Invoice template is missing or empty. Please contact site administrator.', 'essenzia-club-core'),
esc_html__('PDF Template Error', 'essenzia-club-core'),
['response' => 500, 'back_link' => true]
);
}
// --- Generate PDF using Dompdf ---
try {
$options = new DompdfOptions();
// Enable remote content if your logo or CSS are external URLs
$options->set('isRemoteEnabled', true);
// Set default font for better UTF-8 character support
$options->set('defaultFont', 'DejaVu Sans'); // Ensure Dompdf can find this font or has a fallback
// Optional: Set chroot for security if loading local files, ensure paths are within chroot
// $options->setChroot(WP_CONTENT_DIR); // Example: Restrict to wp-content
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html_content);
$dompdf->setPaper('A4', 'portrait'); // Standard A4 size, portrait orientation
$dompdf->render(); // Render the HTML to PDF
$filename_base = 'Essenzia-Payout-Invoice';
$filename = sanitize_file_name($filename_base . '-' . $log_entry->id . '-' . date('Y-m-d') . '.pdf');
// Stream the PDF to the browser for download. This will stop script execution.
// "Attachment" => true prompts download, false attempts to display inline.
$dompdf->stream($filename, ["Attachment" => true]);
exit;
} catch (Exception $e) {
error_log("Essenzia Invoice PDF Generation Error: " . $e->getMessage());
wp_die(
esc_html__('An error occurred while generating the PDF invoice. Please try again later or contact support.', 'essenzia-club-core') . ' ' . esc_html($e->getMessage()) . '',
esc_html__('PDF Generation Error', 'essenzia-club-core'),
['response' => 500, 'back_link' => true]
);
}
}
}
// ===== END OF class-invoices.php =====
// ===== START OF class-payouts.php =====
/**
* Essenzia Club Core: Payouts Management
* Handles affiliate withdrawal requests, validation, and logging.
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Payouts {
public static function init() {
// Handle the submission of the withdrawal request form from the frontend dashboard.
// Hooked to 'init' to catch POST requests early.
add_action('init', [self::class, 'handle_frontend_withdrawal_form_submission']);
// Alternative for admin-post handling (if form action is admin-post.php):
// add_action('admin_post_nopriv_essenzia_request_withdrawal', [self::class, 'handle_frontend_withdrawal_form_submission']); // For non-logged in users (though our form requires login)
// add_action('admin_post_essenzia_request_withdrawal', [self::class, 'handle_frontend_withdrawal_form_submission']); // For logged-in users
}
/**
* Handles the submission of the withdrawal request form from the frontend affiliate dashboard.
* Validates the request and logs it if valid.
*/
public static function handle_frontend_withdrawal_form_submission() {
// Check if our specific form was submitted ('essenzia_withdraw_submit' is the name of the submit button in the original template)
// and if the user is logged in.
if (!isset($_POST['essenzia_withdraw_submit']) || !is_user_logged_in()) {
return;
}
// Verify nonce for security. Matches nonce in the withdraw form template.
// The nonce action is 'essenzia_request_withdrawal_action'.
if (!isset($_POST['essenzia_withdraw_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['essenzia_withdraw_nonce'])), 'essenzia_request_withdrawal_action')) {
// Redirect back to the form with an error message and an anchor to the form section.
wp_redirect(add_query_arg(['withdraw_error' => 'security_check_failed'], wp_get_referer() . '#essenzia-withdraw-form-section')); // Ensure anchor matches section ID
exit;
}
$current_user_id = get_current_user_id();
// Sanitize and validate withdrawal amount
// Allows comma or dot as decimal separator, then converts to float.
$requested_amount_raw = isset($_POST['withdraw_amount']) ? sanitize_text_field(wp_unslash($_POST['withdraw_amount'])) : '0';
$requested_amount = floatval(str_replace(',', '.', $requested_amount_raw));
// Sanitize withdrawal note (optional)
$withdrawal_note = isset($_POST['withdraw_note']) ? sanitize_textarea_field(wp_unslash($_POST['withdraw_note'])) : '';
// --- Validation Checks ---
// 1. Amount must be a positive number.
if ($requested_amount <= 0) {
wp_redirect(add_query_arg(['withdraw_error' => 'invalid_amount'], wp_get_referer() . '#essenzia-withdraw-form-section'));
exit;
}
// 2. Check against minimum withdrawal amount (from plugin settings).
$min_withdrawal_amount = floatval(get_option('essenzia_min_withdrawal_amount', 20)); // Default 20 from settings
if ($requested_amount < $min_withdrawal_amount) {
wp_redirect(add_query_arg(['withdraw_error' => 'min_amount_not_met', 'min' => $min_withdrawal_amount], wp_get_referer() . '#essenzia-withdraw-form-section'));
exit;
}
// 3. Check if user has sufficient balance.
// essenzia_get_affiliate_balance() already considers existing pending withdrawals.
$current_balance = essenzia_get_affiliate_balance($current_user_id);
if ($requested_amount > $current_balance) {
wp_redirect(add_query_arg(['withdraw_error' => 'insufficient_funds', 'balance' => $current_balance], wp_get_referer() . '#essenzia-withdraw-form-section'));
exit;
}
// 4. Check for existing *pending* withdrawal requests to prevent multiple simultaneous pending requests.
if (self::has_pending_withdrawal_request($current_user_id)) {
wp_redirect(add_query_arg(['withdraw_error' => 'pending_exists'], wp_get_referer() . '#essenzia-withdraw-form-section'));
exit;
}
// --- Log the withdrawal request ---
global $wpdb;
$logs_table_name = $wpdb->prefix . 'essenzia_logs';
$log_data = [
'user_id' => $current_user_id,
'action_type' => 'withdraw_request', // Specific action type for withdrawals
'amount' => $requested_amount, // This is the amount requested (a positive value representing a debit from balance)
'note' => $withdrawal_note, // User-provided note for the request
'status' => Essenzia_Users::STATUS_PENDING, // New requests are 'pending' admin approval
'created_at' => current_time('mysql', 1), // GMT/UTC time
'updated_at' => current_time('mysql', 1), // GMT/UTC time
];
$log_formats = ['%d', '%s', '%f', '%s', '%s', '%s', '%s']; // Data formats for $wpdb->insert
$inserted = $wpdb->insert($logs_table_name, $log_data, $log_formats);
if ($inserted) {
// Notify admin about the new withdrawal request
do_action('essenzia_notify_admin_new_withdrawal_request', $current_user_id, $requested_amount, $withdrawal_note);
wp_redirect(add_query_arg(['withdraw_status' => 'success'], wp_get_referer() . '#essenzia-withdraw-form-section'));
} else {
// Log error and redirect with a general failure message
error_log(__CLASS__ . ": Failed to insert withdrawal request for User ID: {$current_user_id}. Amount: {$requested_amount}. DB Error: " . $wpdb->last_error);
wp_redirect(add_query_arg(['withdraw_status' => 'failed', 'dberror' => '1'], wp_get_referer() . '#essenzia-withdraw-form-section'));
}
exit; // Always exit after a redirect
}
/**
* Checks if a user has any existing PENDING withdrawal requests.
*
* @param int $user_id The user ID.
* @return bool True if a pending request exists, false otherwise.
*/
public static function has_pending_withdrawal_request($user_id) {
global $wpdb;
$logs_table_name = $wpdb->prefix . 'essenzia_logs';
$user_id = intval($user_id);
if ($user_id <= 0) return false;
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(id) FROM {$logs_table_name} WHERE user_id = %d AND action_type = 'withdraw_request' AND status = %s",
$user_id,
Essenzia_Users::STATUS_PENDING // Use constant for 'pending'
));
return $count > 0;
}
/**
* Gets withdrawal requests logs. Can be used for user's own logs or admin view.
*
* @param int|null $user_id Optional. User ID. If null or 0, implies admin view of all users (capability check needed).
* @param string|null $status Optional. Filter by status (e.g., 'pending', 'paid', 'all'). Use Essenzia_Users constants.
* @param int $limit Optional. Number of logs per page. Default 20.
* @param int $offset Optional. Offset for pagination. Default 0.
* @return array Array of withdrawal log objects. Returns empty array if unauthorized or no logs.
*/
public static function get_withdrawal_requests_logs($user_id = null, $status = null, $limit = 20, $offset = 0) {
global $wpdb;
$logs_table_name = $wpdb->prefix . 'essenzia_logs';
$user_id = intval($user_id); // Ensure user_id is an integer
$limit = max(1, intval($limit));
$offset = max(0, intval($offset));
$sql_where_clauses = ["action_type = %s"];
$params = ['withdraw_request'];
if ($user_id > 0) { // Specific user's logs
$sql_where_clauses[] = "user_id = %d";
$params[] = $user_id;
} else { // No specific user ID means fetching for all users (admin context)
// Capability check for viewing all users' withdrawal requests
if (!current_user_can('manage_options')) { // Or a more specific capability like 'manage_affiliate_withdrawals'
return []; // Return empty array if not authorized
}
}
if ($status !== null && $status !== 'all' && !empty($status) && array_key_exists($status, Essenzia_Users::get_affiliate_status_options())) {
// Ensure status is valid if it's not 'all'. Here, Essenzia_Users status options might not perfectly match log statuses.
// We'll assume 'pending', 'paid', 'rejected' are the primary ones from Essenzia_Users.
$sql_where_clauses[] = "status = %s";
$params[] = sanitize_key($status);
}
$where_sql = implode(' AND ', $sql_where_clauses);
$sql = "SELECT * FROM {$logs_table_name} WHERE {$where_sql} ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
return $wpdb->get_results($wpdb->prepare($sql, ...$params));
}
}
// ===== END OF class-payouts.php =====
// ===== START OF class-privacy.php =====
/**
* Essenzia Club Core: Privacy and GDPR Compliance
* Handles integration with WordPress Personal Data Export and Erasure tools.
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Privacy {
public static function init() {
// Register data exporter
add_filter('wp_privacy_personal_data_exporters', [self::class, 'register_data_exporter'], 10);
// Register data eraser
add_filter('wp_privacy_personal_data_erasers', [self::class, 'register_data_eraser'], 10);
}
/**
* Registers the data exporter for Essenzia Club Core.
*
* @param array $exporters Array of registered exporters.
* @return array Modified array of exporters.
*/
public static function register_data_exporter($exporters) {
$exporters['essenzia-club-core'] = [
'exporter_friendly_name' => __('Essenzia Affiliate Data', 'essenzia-club-core'),
'callback' => [self::class, 'export_user_data'],
];
return $exporters;
}
/**
* Registers the data eraser for Essenzia Club Core.
*
* @param array $erasers Array of registered erasers.
* @return array Modified array of erasers.
*/
public static function register_data_eraser($erasers) {
$erasers['essenzia-club-core'] = [
'eraser_friendly_name' => __('Essenzia Affiliate Data', 'essenzia-club-core'),
'callback' => [self::class, 'erase_user_data'],
];
return $erasers;
}
/**
* Retrieves a list of affiliate-related user meta keys.
*
* @return array List of meta keys.
*/
private static function get_affiliate_user_meta_keys() {
$meta_keys = [
Essenzia_Users::AFFILIATE_STATUS_META_KEY,
'essenzia_payout_method', // Assuming this is the meta key used for payout method
'essenzia_payout_details',// Assuming this is the meta key for payout details
];
// Add referral chain meta keys dynamically based on max levels
$max_levels = intval(get_option('essenzia_max_affiliate_levels', 3));
for ($i = 1; $i <= $max_levels; $i++) {
$meta_keys[] = 'essenzia_referrer_l' . $i;
}
return $meta_keys;
}
/**
* Callback for exporting user data.
*
* @param string $email_address The email address of the user.
* @param int $page The page number for pagination (not used here).
* @return array An array of export data.
*/
public static function export_user_data($email_address, $page = 1) {
$export_items = [];
$user = get_user_by('email', $email_address);
if (!$user) {
return ['data' => $export_items, 'done' => true];
}
$user_id = $user->ID;
$group_id = 'essenzia-affiliate-data'; // Unique group ID for this plugin's data
$group_label = __('Essenzia Affiliate Data', 'essenzia-club-core');
// Export user meta
$meta_to_export = [];
$affiliate_meta_keys = self::get_affiliate_user_meta_keys();
foreach ($affiliate_meta_keys as $meta_key) {
$meta_value = get_user_meta($user_id, $meta_key, true);
if (!empty($meta_value)) {
// Provide more user-friendly names for meta keys
$friendly_name = ucwords(str_replace(['essenzia_', '_'], ['', ' '], $meta_key));
$meta_to_export[] = ['name' => esc_html($friendly_name), 'value' => esc_html($meta_value)];
}
}
if (!empty($meta_to_export)) {
$export_items[] = [
'group_id' => $group_id,
'group_label' => $group_label,
'item_id' => "user-meta-{$user_id}",
'data' => $meta_to_export,
];
}
// Export logs from custom table
global $wpdb;
$logs_table = $wpdb->prefix . 'essenzia_logs';
$user_logs = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$logs_table} WHERE user_id = %d", $user_id), ARRAY_A);
if (!empty($user_logs)) {
$log_data_to_export = [];
foreach ($user_logs as $index => $log_entry) {
$log_item_data = [];
foreach ($log_entry as $key => $value) {
if ($key === 'user_id') continue; // User ID is already known
$friendly_key = ucwords(str_replace('_', ' ', $key));
$log_item_data[] = ['name' => esc_html($friendly_key), 'value' => esc_html($value)];
}
$log_data_to_export[] = [
'group_id' => $group_id . '-logs',
'group_label' => __('Affiliate Activity Logs', 'essenzia-club-core'),
'item_id' => "log-{$log_entry['id']}",
'data' => $log_item_data,
];
}
$export_items = array_merge($export_items, $log_data_to_export);
}
return [
'data' => $export_items,
'done' => true, // All data exported in one go
];
}
/**
* Callback for erasing user data.
*
* @param string $email_address The email address of the user.
* @param int $page The page number for pagination (not used here).
* @return array An array of results.
*/
public static function erase_user_data($email_address, $page = 1) {
$items_removed = false;
$items_retained = false;
$messages = [];
$user = get_user_by('email', $email_address);
if (!$user) {
$messages[] = __('User not found for the provided email address.', 'essenzia-club-core');
return ['items_removed' => false, 'items_retained' => false, 'messages' => $messages, 'done' => true];
}
$user_id = $user->ID;
// Erase user meta
$affiliate_meta_keys = self::get_affiliate_user_meta_keys();
foreach ($affiliate_meta_keys as $meta_key) {
if (delete_user_meta($user_id, $meta_key)) {
$items_removed = true;
}
}
if ($items_removed) {
$messages[] = __('Affiliate specific user profile data removed.', 'essenzia-club-core');
}
// Anonymize logs in custom table
global $wpdb;
$logs_table = $wpdb->prefix . 'essenzia_logs';
// Instead of deleting, anonymize by setting user_id to 0 (or a placeholder ID if you have one)
// and clearing potentially personal notes.
$anonymized_data = [
'user_id' => 0, // Anonymize user ID
'note' => __('User data erased.', 'essenzia-club-core') // Anonymize note
];
$where_data = ['user_id' => $user_id];
$anonymized_count = $wpdb->update(
$logs_table,
$anonymized_data,
$where_data,
['%d', '%s'], // Format for anonymized_data
['%d'] // Format for where_data
);
if ($anonymized_count > 0) {
$items_removed = true; // Considered removed from user's perspective
$messages[] = sprintf(
/* translators: %d: number of log entries */
_n(
'%d affiliate activity log entry anonymized.',
'%d affiliate activity log entries anonymized.',
$anonymized_count,
'essenzia-club-core'
),
$anonymized_count
);
} elseif ($anonymized_count === false) { // $wpdb->update returns false on error
$items_retained = true;
$messages[] = __('Could not anonymize affiliate activity logs due to a database error.', 'essenzia-club-core');
error_log("Essenzia Privacy Eraser: Failed to anonymize logs for user ID {$user_id}. DB Error: " . $wpdb->last_error);
}
// Example: Retaining certain data for legal reasons
// if (some_condition_to_retain_data()) {
// $items_retained = true;
// $messages[] = __('Certain affiliate transaction summaries are retained for financial auditing purposes for X years.', 'essenzia-club-core');
// }
return [
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => true, // All processing done in one go
];
}
}
// ===== END OF class-privacy.php =====
// ===== START OF class-referrals.php =====
/**
* Essenzia Club Core: Referral Tracking & Management
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Referrals {
const REFERRAL_COOKIE_NAME = 'essenzia_affiliate_ref_id';
const REFERRAL_PARAM = 'ref';
public static function init() {
// Capture referral ID early, before most other actions. template_redirect is a good hook.
add_action('template_redirect', [self::class, 'capture_referral_id_from_url'], 1);
// Store referral chain and set initial status when a new user registers.
add_action('user_register', [self::class, 'handle_new_user_registration'], 10, 1);
}
/**
* Captures the referrer's ID from the URL query parameter and sets a cookie.
*/
public static function capture_referral_id_from_url() {
// If a logged-in user visits a referral link, by default, don't set/override their potential existing referral cookie
// unless they are the referrer themselves (self-referral, typically disallowed for commissions but cookie can be set).
// This filter can allow overriding this behavior.
if (is_user_logged_in() && !apply_filters('essenzia_allow_logged_in_user_to_be_cookied', false)) {
// If the logged-in user IS the referrer in the link, allow cookie setting (will be handled by self-referral checks later)
if (isset($_GET[self::REFERRAL_PARAM]) && get_current_user_id() != intval($_GET[self::REFERRAL_PARAM])) {
return;
}
}
if (isset($_GET[self::REFERRAL_PARAM])) {
$referrer_id = intval($_GET[self::REFERRAL_PARAM]);
// If referrer ID is invalid (0 or non-existent user), clear any existing referral cookie.
if ($referrer_id <= 0 || !get_userdata($referrer_id)) {
self::clear_referral_cookie();
return;
}
// Prevent self-referral cookie setting if a logged-in user clicks their own link,
// unless a filter allows it (e.g., for testing).
if (is_user_logged_in() && get_current_user_id() == $referrer_id && !apply_filters('essenzia_allow_self_referral_cookie_on_own_link', false)) {
return; // Do not set cookie for self-referral
}
$cookie_validity_days = max(1, intval(get_option('essenzia_cookie_validity_days', 30)));
$cookie_expiry_time = time() + ($cookie_validity_days * DAY_IN_SECONDS);
$secure_cookie = apply_filters('essenzia_referral_cookie_secure', is_ssl());
$samesite_policy = apply_filters('essenzia_referral_cookie_samesite', 'Lax'); // Lax is usually fine
$options = [
'expires' => $cookie_expiry_time,
'path' => COOKIEPATH ?: '/', // Path WordPress is installed in
'domain' => COOKIE_DOMAIN, // Current domain
'secure' => $secure_cookie,
'httponly' => true, // Prevent JavaScript access to the cookie
'samesite' => $samesite_policy // Helps prevent CSRF
];
// Set the cookie with the referrer's ID
setcookie(self::REFERRAL_COOKIE_NAME, $referrer_id, $options);
}
}
/**
* Handles actions upon new user registration: stores referral chain and sets initial affiliate status.
*
* @param int $new_user_id The ID of the newly registered user.
*/
public static function handle_new_user_registration($new_user_id) {
self::store_referral_chain($new_user_id);
// Set initial affiliate status based on plugin settings (pending/approved)
if (class_exists('Essenzia_Users')) {
Essenzia_Users::set_initial_affiliate_status($new_user_id);
} else {
// Fallback if Essenzia_Users class isn't loaded (should not happen with proper loading)
error_log('Essenzia Club Core: Essenzia_Users class not found during user registration status setting for user ID ' . $new_user_id);
// Implement a basic fallback if strictly necessary, though fixing load order is preferred
$manual_approval = (get_option('essenzia_manual_approval_enabled', 'yes') === 'yes');
$initial_status = $manual_approval ? 'pending' : 'approved';
update_user_meta($new_user_id, 'essenzia_affiliate_status', $initial_status);
}
}
/**
* Stores the referral chain (upline) for a newly registered user based on the referral cookie.
* Also checks for the maximum direct referrals limit.
*
* @param int $new_user_id The ID of the newly registered user.
*/
private static function store_referral_chain($new_user_id) {
if (!isset($_COOKIE[self::REFERRAL_COOKIE_NAME])) {
return; // No referral cookie found
}
$referrer_l1_id = intval($_COOKIE[self::REFERRAL_COOKIE_NAME]);
// Validate the L1 referrer
if ($referrer_l1_id <= 0 || $referrer_l1_id == $new_user_id || !get_userdata($referrer_l1_id)) {
self::clear_referral_cookie(); // Invalid L1 ID, clear cookie
return;
}
// Check if L1 referrer is an approved affiliate (optional, but good practice)
// if (!essenzia_is_approved_affiliate($referrer_l1_id)) {
// error_log(__CLASS__ . ": L1 Referrer ID {$referrer_l1_id} is not an approved affiliate. No referral chain stored for new user {$new_user_id}.");
// self::clear_referral_cookie();
// return;
// }
// Check L1 referrer's max direct referrals limit
$max_direct_referrals = intval(get_option('essenzia_max_direct_referrals', 0)); // 0 for unlimited
if ($max_direct_referrals > 0) {
if (function_exists('essenzia_count_direct_referrals')) {
$current_direct_referrals = essenzia_count_direct_referrals($referrer_l1_id);
if ($current_direct_referrals >= $max_direct_referrals) {
error_log(__CLASS__ . ": L1 Referrer ID {$referrer_l1_id} has reached the maximum direct referral limit of {$max_direct_referrals}. New user {$new_user_id} not assigned this referrer.");
self::clear_referral_cookie(); // Clear cookie as this referral cannot be processed
return; // Do not assign this L1 referrer or build chain
}
} else {
error_log(__CLASS__ . ": essenzia_count_direct_referrals() function not found. Max direct referrals check skipped for L1 referrer ID {$referrer_l1_id}.");
}
}
// Store L1 referrer
update_user_meta($new_user_id, 'essenzia_referrer_l1', $referrer_l1_id);
// Build and store the rest of the upline chain up to max_levels
$max_levels = max(1, intval(get_option('essenzia_max_affiliate_levels', 3)));
$current_upline_member_id = $referrer_l1_id; // Start with L1 to find L2
for ($level = 2; $level <= $max_levels; $level++) {
// Get the L1 referrer of the current upline member to find the next level up
$next_upline_member_id = intval(get_user_meta($current_upline_member_id, 'essenzia_referrer_l1', true));
// Validate next upline member
if ($next_upline_member_id <= 0 || // Invalid ID
$next_upline_member_id == $new_user_id || // Cannot be the new user themselves
$next_upline_member_id == $current_upline_member_id || // Prevent self-reference in chain
!get_userdata($next_upline_member_id)) { // User does not exist
break; // End of chain or invalid member
}
// Anti-loop: Check if this $next_upline_member_id is already in the $new_user_id's upline
$is_already_in_upline = false;
for ($k = 1; $k < $level; $k++) { // Check levels already set for $new_user_id
if (intval(get_user_meta($new_user_id, 'essenzia_referrer_l' . $k, true)) == $next_upline_member_id) {
$is_already_in_upline = true;
break;
}
}
if ($is_already_in_upline) {
error_log(__CLASS__ . ": Referral loop detected for new user {$new_user_id} at level {$level} with potential upline member {$next_upline_member_id}. Halting chain construction.");
break; // Break to prevent loop
}
// Store this upline member for the new user at the current level
update_user_meta($new_user_id, 'essenzia_referrer_l' . $level, $next_upline_member_id);
// Move up the chain for the next iteration
$current_upline_member_id = $next_upline_member_id;
}
// Clear the referral cookie after processing
self::clear_referral_cookie();
}
/**
* Clears the referral cookie.
*/
private static function clear_referral_cookie() {
if (isset($_COOKIE[self::REFERRAL_COOKIE_NAME])) {
$options = [
'expires' => time() - YEAR_IN_SECONDS, // Expire in the past
'path' => COOKIEPATH ?: '/',
'domain' => COOKIE_DOMAIN,
'secure' => apply_filters('essenzia_referral_cookie_secure', is_ssl()),
'httponly' => true,
'samesite' => apply_filters('essenzia_referral_cookie_samesite', 'Lax')
];
// Unset from $_COOKIE array for current request and tell browser to delete it
unset($_COOKIE[self::REFERRAL_COOKIE_NAME]);
setcookie(self::REFERRAL_COOKIE_NAME, '', $options);
}
}
}
// ===== END OF class-referrals.php =====
// ===== START OF class-users.php =====
/**
* Essenzia Club Core: User Management for Affiliates
* Handles affiliate status, retrieval of affiliate users, and related user meta.
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
class Essenzia_Users {
/**
* The user meta key for storing an affiliate's status.
*/
const AFFILIATE_STATUS_META_KEY = 'essenzia_affiliate_status';
// Define status constants for consistency and to avoid magic strings.
const STATUS_PENDING = 'pending';
const STATUS_APPROVED = 'approved';
const STATUS_REJECTED = 'rejected';
// const STATUS_INACTIVE = 'inactive'; // Example for future expansion (e.g., if an affiliate becomes dormant)
/**
* Initializes hooks related to user management for affiliates.
* Currently, no direct hooks are initialized by this class itself,
* but it's available for future use.
*/
public static function init() {
// Example: add_action('delete_user', [self::class, 'cleanup_affiliate_data_on_user_delete']);
// Example: add_filter('manage_users_columns', [self::class, 'add_affiliate_status_column']);
// Example: add_filter('manage_users_custom_column', [self::class, 'render_affiliate_status_column_content'], 10, 3);
}
/**
* Get available affiliate status options for dropdowns, UI elements, etc.
*
* @return array Associative array of status_key => status_label.
*/
public static function get_affiliate_status_options() {
return [
self::STATUS_PENDING => __('Pending Approval', 'essenzia-club-core'),
self::STATUS_APPROVED => __('Approved Affiliate', 'essenzia-club-core'),
self::STATUS_REJECTED => __('Rejected', 'essenzia-club-core'),
// self::STATUS_INACTIVE => __('Inactive', 'essenzia-club-core'), // Future use
];
}
/**
* Sets the initial affiliate status for a newly registered user based on plugin settings.
* This is typically called from the `user_register` hook handler in `Essenzia_Referrals`.
*
* @param int $user_id The ID of the newly registered user.
*/
public static function set_initial_affiliate_status($user_id) {
$user_id = intval($user_id);
if (!$user_id || !get_userdata($user_id)) { // Ensure user exists
error_log(__CLASS__ . ": Attempted to set initial status for invalid user ID {$user_id}");
return;
}
// Check if manual approval is required from plugin settings
$manual_approval_enabled = (get_option('essenzia_manual_approval_enabled', 'yes') === 'yes');
$initial_status = $manual_approval_enabled ? self::STATUS_PENDING : self::STATUS_APPROVED;
update_user_meta($user_id, self::AFFILIATE_STATUS_META_KEY, $initial_status);
// If auto-approved, an action can be hooked here for further processing (e.g., welcome email variation)
if ($initial_status === self::STATUS_APPROVED && !$manual_approval_enabled) {
/**
* Fires when a new affiliate is automatically approved upon registration.
*
* @param int $user_id The ID of the auto-approved user.
*/
do_action('essenzia_affiliate_auto_approved', $user_id);
} elseif ($initial_status === self::STATUS_PENDING) {
/**
* Fires when a new affiliate registration is set to pending approval.
*
* @param int $user_id The ID of the user set to pending.
*/
do_action('essenzia_affiliate_pending_approval', $user_id);
}
}
/**
* Updates the affiliate status for a given user.
*
* @param int $user_id The ID of the user.
* @param string $new_status The new status to set (must be a key from get_affiliate_status_options()).
* @return bool True on success, false on failure (e.g., invalid status, DB error, or user not found).
*/
public static function update_affiliate_status($user_id, $new_status) {
$user_id = intval($user_id);
if (!$user_id || !get_userdata($user_id)) {
error_log(__CLASS__ . ": Attempted to update status for invalid user ID {$user_id}");
return false;
}
$valid_statuses = array_keys(self::get_affiliate_status_options());
if (!in_array($new_status, $valid_statuses, true)) {
error_log(__CLASS__ . ": Attempted to set invalid affiliate status '{$new_status}' for user ID {$user_id}");
return false;
}
$old_status = get_user_meta($user_id, self::AFFILIATE_STATUS_META_KEY, true);
// If the status is the same, no update is needed, but we can return true as the "state" is correct.
if ($old_status === $new_status) {
return true;
}
$result = update_user_meta($user_id, self::AFFILIATE_STATUS_META_KEY, $new_status);
if ($result) {
// Action hook for when an affiliate's status is changed.
// Could be used for logging, sending notifications, etc.
// Note: Notifications are handled via `essenzia_notify_user_affiliate_status_change` in Essenzia_Admin.
do_action('essenzia_affiliate_status_updated', $user_id, $new_status, $old_status);
return true;
}
// If update_user_meta returns false, it means the value was the same or an error occurred.
// Since we check for $old_status === $new_status, a false here likely means a DB error if $result was not an ID.
error_log(__CLASS__ . ": Failed to update affiliate status for user ID {$user_id} from '{$old_status}' to '{$new_status}'. update_user_meta returned: " . print_r($result, true));
return false;
}
/**
* Retrieves users based on various criteria, including affiliate status.
* This is a wrapper for WP_User_Query to simplify fetching affiliates.
*
* @param array $args Arguments for WP_User_Query.
* Includes custom 'affiliate_status' argument which can be:
* - A specific status (e.g., 'pending', 'approved').
* - 'all_with_status' to get users with any essenzia_affiliate_status meta key.
* - null or 'all' (default WP_User_Query behavior, may show all users).
* @return WP_User_Query|null The WP_User_Query object, or null if query args are problematic.
*/
public static function get_affiliates($args = []) {
$defaults = [
'orderby' => 'registered',
'order' => 'DESC',
'number' => 20, // Default users per page
'paged' => 1, // Default to page 1
'count_total'=> true, // Necessary for pagination
'meta_query' => [], // Initialize meta_query array
];
$query_args = wp_parse_args($args, $defaults);
// Handle the custom 'affiliate_status' argument
if (isset($query_args['affiliate_status'])) {
$status_filter = $query_args['affiliate_status'];
unset($query_args['affiliate_status']); // Remove from args to prevent WP_User_Query conflict
if ($status_filter === 'all_with_status') {
$query_args['meta_query'][] = [
'key' => self::AFFILIATE_STATUS_META_KEY,
'compare' => 'EXISTS', // User has the meta key, regardless of value
];
} elseif (array_key_exists($status_filter, self::get_affiliate_status_options())) {
$query_args['meta_query'][] = [
'key' => self::AFFILIATE_STATUS_META_KEY,
'value' => sanitize_key($status_filter),
'compare' => '=',
];
}
// If 'all' or an invalid status, no specific affiliate status meta query is added here from this custom arg.
}
// Ensure 'relation' is set if multiple meta queries exist
if (count($query_args['meta_query']) > 1 && !isset($query_args['meta_query']['relation'])) {
$query_args['meta_query']['relation'] = 'AND';
}
return new WP_User_Query($query_args);
}
/**
* Checks if a user is an approved affiliate.
*
* @param int $user_id The user ID.
* @return bool True if the user has 'approved' status, false otherwise.
*/
public static function is_user_approved_affiliate($user_id) {
if (!$user_id || intval($user_id) <= 0) {
return false;
}
$status = get_user_meta(intval($user_id), self::AFFILIATE_STATUS_META_KEY, true);
return $status === self::STATUS_APPROVED;
}
}
// ===== END OF class-users.php =====
// ===== START OF functions-utils.php =====
/**
* Essenzia Club Core: Utility Functions
*
* @package EssenziaClubCore
*/
if (!defined('ABSPATH')) exit; // Exit if accessed directly
/**
* Checks if a user has an active subscription.
* Requires the WooCommerce Subscriptions plugin to be active.
*
* @param int $user_id The user ID to check.
* @return bool True if the user has at least one active subscription, false otherwise.
*/
function essenzia_user_has_active_subscription($user_id) { //
if (!function_exists('wcs_user_has_subscription')) {
// WooCommerce Subscriptions plugin is not active, so we cannot check.
// For access control, failing closed (returning false) is safer.
return false;
}
// wcs_user_has_subscription checks if a user has a subscription with ANY status.
// The second parameter '' means for any product.
// The third parameter 'active' checks for a specific status.
return wcs_user_has_subscription($user_id, '', 'active');
}
/**
* Get total available balance for an affiliate.
* Considers approved commissions minus 'paid' and 'pending' withdrawals.
*
* @param int $user_id The ID of the user.
* @return float The calculated balance, ensuring it's not negative.
*/
function essenzia_get_affiliate_balance($user_id) {
global $wpdb;
$logs_table = $wpdb->prefix . 'essenzia_logs';
$user_id = intval($user_id);
if (!$user_id) return 0.0;
// Total approved commissions
$earned_query = $wpdb->prepare("
SELECT SUM(amount)
FROM {$logs_table}
WHERE user_id = %d
AND action_type LIKE %s
AND status = %s
", $user_id, 'referral_commission_l%', 'approved');
$total_earned = (float) $wpdb->get_var($earned_query);
// Total deducted for withdrawals
$withdrawn_query = $wpdb->prepare("
SELECT SUM(amount)
FROM {$logs_table}
WHERE user_id = %d
AND action_type = %s
AND (status = %s OR status = %s)
", $user_id, 'withdraw_request', 'paid', 'pending');
$total_deducted_for_withdrawals = (float) $wpdb->get_var($withdrawn_query);
$balance = $total_earned - $total_deducted_for_withdrawals;
return max(0, round($balance, 2));
}
// ... (the rest of the functions in this file remain the same) ...
/**
* Get affiliate logs by user and optionally by action type or status.
*/
function essenzia_get_user_logs($user_id, $action_type = null, $status = null, $limit = 20, $offset = 0, $orderby = 'created_at', $order = 'DESC') {
global $wpdb;
$logs_table = $wpdb->prefix . 'essenzia_logs';
$user_id = intval($user_id);
if (!$user_id && $user_id !== 0) {
return [];
}
$limit = max(1, intval($limit));
$offset = max(0, intval($offset));
$allowed_orderby = ['id', 'action_type', 'amount', 'status', 'created_at', 'updated_at'];
$orderby = in_array($orderby, $allowed_orderby, true) ? $orderby : 'created_at';
$order = (strtoupper($order) === 'ASC') ? 'ASC' : 'DESC';
$sql_where_clauses = [];
$params = [];
if ($user_id > 0) {
$sql_where_clauses[] = "user_id = %d";
$params[] = $user_id;
}
if ($action_type !== null) {
$sql_where_clauses[] = (strpos($action_type, '%') !== false) ? "action_type LIKE %s" : "action_type = %s";
$params[] = sanitize_text_field($action_type);
}
if ($status !== null) {
$sql_where_clauses[] = "status = %s";
$params[] = sanitize_key($status);
}
$where_sql = !empty($sql_where_clauses) ? "WHERE " . implode(' AND ', $sql_where_clauses) : "";
$sql = "SELECT * FROM {$logs_table} {$where_sql} ORDER BY {$orderby} {$order}, id DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
return $wpdb->get_results($wpdb->prepare($sql, ...$params));
}
/**
* Format an amount into a currency string.
*/
function essenzia_format_money($amount, $args = []) {
$amount = floatval($amount);
if (isset($args['currency_symbol_only']) && $args['currency_symbol_only'] === true) {
if (function_exists('get_woocommerce_currency_symbol')) {
return get_woocommerce_currency_symbol();
}
return isset($args['currency']) ? $args['currency'] : 'AED';
}
if (function_exists('wc_price')) {
return wc_price($amount, $args);
}
$currency_symbol = isset($args['currency']) ? $args['currency'] : (function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : 'AED');
$decimals = isset($args['decimals']) ? intval($args['decimals']) : (function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2);
$decimal_sep = isset($args['decimal_separator']) ? $args['decimal_separator'] : (function_exists('wc_get_price_decimal_separator') ? wc_get_price_decimal_separator() : '.');
$thousands_sep = isset($args['thousand_separator']) ? $args['thousand_separator'] : (function_exists('wc_get_price_thousand_separator') ? wc_get_price_thousand_separator() : ',');
$formatted_number = number_format($amount, $decimals, $decimal_sep, $thousands_sep);
return $currency_symbol . ' ' . $formatted_number;
}
/**
* Format a datetime string.
*/
function essenzia_format_date($datetime_string, $format = '') {
if (empty($datetime_string) || $datetime_string === '0000-00-00 00:00:00' || $datetime_string === null) {
return __('N/A', 'essenzia-club-core');
}
if (empty($format)) {
$format = get_option('date_format') . ' ' . get_option('time_format');
}
try {
$date_obj = new DateTime($datetime_string, new DateTimeZone('GMT'));
if (function_exists('wp_timezone')) {
$date_obj->setTimezone(wp_timezone());
} else {
$date_obj->setTimezone(new DateTimeZone(get_option('timezone_string')));
}
return $date_obj->format($format);
} catch (Exception $e) {
$timestamp = strtotime($datetime_string);
if ($timestamp === false) {
return __('Invalid Date', 'essenzia-club-core');
}
return date_i18n($format, $timestamp, true);
}
}
/**
* Checks if a user is an approved affiliate.
*/
function essenzia_is_approved_affiliate($user_id) {
if (class_exists('Essenzia_Users')) {
return Essenzia_Users::is_user_approved_affiliate($user_id);
}
if (!$user_id || intval($user_id) <= 0) return false;
$status = get_user_meta(intval($user_id), 'essenzia_affiliate_status', true);
return $status === 'approved';
}
/**
* Gets the URL for the main affiliate dashboard page.
*/
function essenzia_get_affiliate_dashboard_url($sub_page_slug = '') {
$dashboard_page_slug = apply_filters('essenzia_dashboard_page_slug', 'affiliate-dashboard');
$dashboard_page = get_page_by_path($dashboard_page_slug);
if ($dashboard_page) {
$base_url = get_permalink($dashboard_page->ID);
if (!empty($sub_page_slug)) {
return trailingslashit($base_url) . user_trailingslashit(sanitize_key($sub_page_slug), 'category');
}
return $base_url;
}
return home_url(user_trailingslashit($dashboard_page_slug));
}
/**
* Gets the URL for the affiliate login page.
*/
function essenzia_get_affiliate_login_url() {
$login_page_slug = apply_filters('essenzia_login_page_slug', 'affiliate-login');
$login_page = get_page_by_path($login_page_slug);
$redirect_after_login = essenzia_get_affiliate_dashboard_url();
return $login_page ? get_permalink($login_page->ID) : wp_login_url($redirect_after_login);
}
/**
* Gets the URL for the affiliate registration page.
*/
function essenzia_get_affiliate_register_url() {
$register_page_slug = apply_filters('essenzia_register_page_slug', 'affiliate-register');
$register_page = get_page_by_path($register_page_slug);
return $register_page ? get_permalink($register_page->ID) : wp_registration_url();
}
/**
* Counts the number of direct (Level 1) referrals for a given referrer.
*/
function essenzia_count_direct_referrals($referrer_id) {
global $wpdb;
$referrer_id = intval($referrer_id);
if ($referrer_id <= 0) {
return 0;
}
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(user_id) FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value = %d",
'essenzia_referrer_l1',
$referrer_id
));
return intval($count);
}
/**
* Get total number of successful referrals (Level 1 only) for a user.
*/
function essenzia_get_total_referrals_count($user_id) {
$user_id = intval($user_id);
if ($user_id <= 0) return 0;
return essenzia_count_direct_referrals($user_id);
}
/**
* STUB: Get total clicks on referral links for a user.
*/
function essenzia_get_total_clicks_count($user_id) {
$user_id = intval($user_id);
return 0; // Placeholder
}
/**
* Calculate conversion rate (referrals per click).
*/
function essenzia_get_conversion_rate($user_id) {
$user_id = intval($user_id);
if ($user_id <= 0) return 0.0;
$total_referrals = essenzia_get_total_referrals_count($user_id);
$total_clicks = essenzia_get_total_clicks_count($user_id);
if ($total_clicks > 0) {
return round(($total_referrals / $total_clicks) * 100, 2);
}
return 0.0;
}
/**
* Get total amount paid out to an affiliate.
*/
function essenzia_get_total_paid_earnings($user_id) {
global $wpdb;
$logs_table = $wpdb->prefix . 'essenzia_logs';
$user_id = intval($user_id);
if ($user_id <= 0) return 0.0;
$total_paid = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(amount) FROM {$logs_table} WHERE user_id = %d AND action_type = %s AND status = %s",
$user_id,
'withdraw_request',
'paid'
));
return round(floatval($total_paid), 2);
}
/**
* Counts logs based on user ID and optional filters.
*/
function essenzia_count_user_logs($user_id, $action_type = null, $status = null) {
global $wpdb;
$logs_table = $wpdb->prefix . 'essenzia_logs';
$user_id = intval($user_id);
if (!$user_id && $user_id !== 0) {
return 0;
}
$sql_where_clauses = [];
$params = [];
if ($user_id > 0) {
$sql_where_clauses[] = "user_id = %d";
$params[] = $user_id;
}
if ($action_type !== null) {
$sql_where_clauses[] = (strpos($action_type, '%') !== false) ? "action_type LIKE %s" : "action_type = %s";
$params[] = sanitize_text_field($action_type);
}
if ($status !== null) {
$sql_where_clauses[] = "status = %s";
$params[] = sanitize_key($status);
}
$where_sql = !empty($sql_where_clauses) ? "WHERE " . implode(' AND ', $sql_where_clauses) : "";
$count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(id) FROM {$logs_table} {$where_sql}", ...$params));
return intval($count);
}
// ===== END OF functions-utils.php =====
تتركز – MHM StoresSkip to content
Happy Shopping With More Rewards
تتركز
“داوني – منعم الأقمشة المركز المضاد للبكالوريا 1.38 لتر – يحارب البكتيريا والروائح” has been added to your cart. View cart