'per_user', // 'per_user' or 'flat_total' 'referrer_reward_amount' => 25.00, 'referrer_reward_type' => 'fixed', 'referee_reward_type' => 'percentage', // 'percentage' or 'fixed' 'referee_reward_amount' => 20, // 20% or $20 'referee_reward_applies_to' => 'first_order' // 'first_order' or 'all_orders' ]; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->cache = new CacheManager('referrals'); $this->referrals_table = BASE . 'referrals'; $this->rewards_table = BASE . 'referral_rewards'; $this->magic_link = new MagicLinkManager(); // Hook into user registration to track referrals add_action('user_register', [$this, 'processReferral'], 10, 1); // Add meta boxes for admin to manage referrals add_action('show_user_profile', [$this, 'displayUserReferralInfo']); add_action('edit_user_profile', [$this, 'displayUserReferralInfo']); // Save referral code changes add_action('personal_options_update', [$this, 'saveUserReferralCode']); add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']); add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2); add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']); add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); // Schedule cron jobs for reports $this->registerCronJobs(); } public function enqueueScripts():void { $requirements = [ 'jvb-utility', 'jvb-a11y', 'jvb-popup', 'jvb-tabs', ]; if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) { $requirements[] = 'cloudflare-turnstile'; } wp_enqueue_script( 'jvb-referral', JVB_URL . 'assets/js/min/referral.min.js', $requirements, '1.0.0', true ); } /** * Register cron jobs for automated reporting */ protected function registerCronJobs(): void { add_action(BASE . 'referral_daily_report', [$this, 'sendDailyReport']); add_action(BASE . 'referral_weekly_report', [$this, 'sendWeeklyReport']); // Schedule if not already scheduled if (!wp_next_scheduled(BASE . 'referral_daily_report')) { wp_schedule_event(strtotime('tomorrow 9:00'), 'daily', BASE . 'referral_daily_report'); } if (!wp_next_scheduled(BASE . 'referral_weekly_report')) { wp_schedule_event(strtotime('next Monday 9:00'), 'weekly', BASE . 'referral_weekly_report'); } } /** * Generate or get existing referral code for a user * * @param int $user_id * @param string|null $custom_code Optional custom code * @return string|WP_Error */ public function getUserReferralCode(int $user_id, ?string $custom_code = null) { $user = get_user_by('ID', $user_id); if (!$user) { return new WP_Error('invalid_user', 'User not found'); } // Check if user already has a code $existing_code = get_user_meta($user_id, BASE . 'referral_code', true); if ($existing_code && !$custom_code) { return $existing_code; } // Generate new code if custom provided or none exists $code = $custom_code ?: $this->generateReferralCode($user); // Validate uniqueness if ($this->isCodeTaken($code, $user_id)) { return new WP_Error('code_taken', 'This referral code is already in use'); } // Save the code update_user_meta($user_id, BASE . 'referral_code', $code); return $code; } /** * Generate a referral code based on user info * * @param WP_User $user * @return string */ protected function generateReferralCode(WP_User $user): string { // Create code from first and last name $first = sanitize_title($user->first_name ?: 'user'); $last = sanitize_title($user->last_name ?: wp_generate_password(4, false)); $base_code = strtoupper(substr($first, 0, 4) . substr($last, 0, 4)); // Ensure uniqueness $code = $base_code; $suffix = 1; while ($this->isCodeTaken($code)) { $code = $base_code . $suffix; $suffix++; } return $code; } /** * Check if a referral code is already taken * * @param string $code * @param int|null $exclude_user_id * @return bool */ protected function isCodeTaken(string $code, ?int $exclude_user_id = null): bool { $args = [ 'meta_key' => BASE . 'referral_code', 'meta_value' => $code, 'fields' => 'ID', 'number' => 1 ]; if ($exclude_user_id) { $args['exclude'] = [$exclude_user_id]; } $users = get_users($args); return !empty($users); } /** * Track a new referral when user registers * * @param int $user_id */ public function processReferral(int $user_id): void { // Check if user was created via referral magic link $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true); if (!$referral_code) { return; } // Find the referrer $referrer = $this->getUserByReferralCode($referral_code); if (!$referrer) { delete_user_meta($user_id, BASE . 'pending_referral_code'); return; } // Check for duplicates $existing = $this->getReferralByReferee($user_id); if ($existing) { delete_user_meta($user_id, BASE . 'pending_referral_code'); return; } // Create referral record $result = $this->createReferral($referrer->ID, $user_id, $referral_code); if ($result) { // Clean up temp meta delete_user_meta($user_id, BASE . 'pending_referral_code'); // Fire action for tracking do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code); } } /** * Create a referral record in the database * * @param int $referrer_id * @param int $referee_id * @param string $code * @return int|false */ protected function createReferral(int $referrer_id, int $referee_id, string $code) { $user = get_user_by('ID', $referee_id); return $this->wpdb->insert( $this->referrals_table, [ 'referrer_id' => $referrer_id, 'referee_id' => $referee_id, 'referee_name' => $user->display_name, 'referee_email' => $user->user_email, 'referee_phone' => get_user_meta($referee_id, BASE . 'phone', true) ?: '', 'referral_code' => $code, 'status' => 'pending', 'referred_at' => current_time('mysql') ], ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s'] ); } /** * Get user by referral code * * @param string $code * @return WP_User|null */ public function getUserByReferralCode(string $code): ?WP_User { $users = get_users([ 'meta_key' => BASE . 'referral_code', 'meta_value' => $code, 'number' => 1 ]); return $users[0] ?? null; } /** * Get referral record by referee ID * * @param int $referee_id * @return object|null */ public function getReferralByReferee(int $referee_id): ?object { $result = $this->wpdb->get_row($this->wpdb->prepare( "SELECT * FROM {$this->referrals_table} WHERE referee_id = %d", $referee_id )); return $result ?: null; } /** * Mark a referral as treated/rewarded * * @param int $referral_id * @param bool $treated * @return bool */ public function markAsTreated(int $referral_id, bool $treated = true): bool { $status = $treated ? 'treated' : 'pending'; $result = $this->wpdb->update( $this->referrals_table, [ 'status' => $status, 'treated_at' => $treated ? current_time('mysql') : null ], ['id' => $referral_id], ['%s', '%s'], ['%d'] ); if ($result && $treated) { // Create reward records when marking as treated $this->createRewardRecords($referral_id); } return $result !== false; } /** * Create reward records for both referrer and referee * * @param int $referral_id */ protected function createRewardRecords(int $referral_id): void { $referral = $this->wpdb->get_row($this->wpdb->prepare( "SELECT * FROM {$this->referrals_table} WHERE id = %d", $referral_id )); if (!$referral) { return; } $settings = $this->getRewardSettings(); // Create referrer reward $this->wpdb->insert( $this->rewards_table, [ 'referral_id' => $referral_id, 'user_id' => $referral->referrer_id, 'reward_type' => 'referrer', 'amount' => $settings['referrer_reward_amount'], 'status' => 'available', 'created_at' => current_time('mysql') ], ['%d', '%d', '%s', '%f', '%s', '%s'] ); // Create referee reward $referee_amount = $settings['referee_reward_type'] === 'percentage' ? $settings['referee_reward_amount'] // Store as percentage : $settings['referee_reward_amount']; // Store as fixed amount $this->wpdb->insert( $this->rewards_table, [ 'referral_id' => $referral_id, 'user_id' => $referral->referee_id, 'reward_type' => 'referee', 'amount' => $referee_amount, 'reward_calculation' => $settings['referee_reward_type'], 'status' => 'available', 'created_at' => current_time('mysql') ], ['%d', '%d', '%s', '%f', '%s', '%s', '%s'] ); } /** * Get referrals for a user * * @param int $user_id * @param array $args * @return array */ public function getUserReferrals(int $user_id, array $args = []): array { $defaults = [ 'status' => 'all', 'limit' => 100, 'offset' => 0, 'orderby' => 'referred_at', 'order' => 'DESC' ]; $args = wp_parse_args($args, $defaults); $where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id); if ($args['status'] !== 'all') { $where .= $this->wpdb->prepare(" AND status = %s", $args['status']); } $query = "SELECT * FROM {$this->referrals_table} {$where} ORDER BY {$args['orderby']} {$args['order']} LIMIT {$args['limit']} OFFSET {$args['offset']}"; return $this->wpdb->get_results($query); } /** * Get referral statistics for a user * * @param int $user_id * @return array */ public function getUserStats(int $user_id): array { $cache_key = 'stats_' . $user_id; $cached = $this->cache->get($cache_key); if ($cached !== false) { return $cached; } $stats = $this->wpdb->get_row($this->wpdb->prepare( "SELECT COUNT(*) as total_referrals, SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count FROM {$this->referrals_table} WHERE referrer_id = %d", $user_id ), ARRAY_A); // Get total rewards $rewards = $this->wpdb->get_row($this->wpdb->prepare( "SELECT SUM(CASE WHEN status = 'available' THEN amount ELSE 0 END) as available_rewards, SUM(CASE WHEN status = 'redeemed' THEN amount ELSE 0 END) as redeemed_rewards FROM {$this->rewards_table} WHERE user_id = %d AND reward_type = 'referrer'", $user_id ), ARRAY_A); $stats = array_merge($stats, $rewards); $this->cache->set($cache_key, $stats, HOUR_IN_SECONDS); return $stats; } /** * Get top referrers for a time period * * @param int $limit * @param string $period 'day'|'week'|'month'|'all' * @return array */ public function getTopReferrers(int $limit = 10, string $period = 'all'): array { $where = ''; if ($period !== 'all') { $date_where = match($period) { 'day' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)", 'week' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)", 'month' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)", default => "1=1" }; $where = "WHERE {$date_where}"; } $query = "SELECT referrer_id, COUNT(*) as referral_count, SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count FROM {$this->referrals_table} {$where} GROUP BY referrer_id ORDER BY referral_count DESC LIMIT {$limit}"; $results = $this->wpdb->get_results($query); // Enrich with user data foreach ($results as &$result) { $user = get_user_by('ID', $result->referrer_id); $result->user_name = $user ? $user->display_name : 'Unknown'; $result->user_email = $user ? $user->user_email : ''; } return $results; } /** * Send daily report if there are new referrals */ public function sendDailyReport(): void { $yesterday = date('Y-m-d', strtotime('-1 day')); // Get new referrals from yesterday $new_referrals = $this->wpdb->get_results($this->wpdb->prepare( "SELECT r.*, u.display_name as referrer_name FROM {$this->referrals_table} r JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID WHERE DATE(r.referred_at) = %s ORDER BY r.referred_at DESC", $yesterday )); // Only send if there's at least 1 new referral if (empty($new_referrals)) { return; } // Build email content $content = '
' . count($new_referrals) . ' new referral' . (count($new_referrals) !== 1 ? 's' : '') . ' yesterday (' . $yesterday . ')
'; $content .= '| Referee | '; $content .= 'Referrer | '; $content .= 'Code | '; $content .= '|
|---|---|---|---|
| %s | ', esc_html($ref->referee_name)); $content .= sprintf('%s | ', esc_html($ref->referee_email)); $content .= sprintf('%s | ', esc_html($ref->referrer_name)); $content .= sprintf('%s | ', esc_html($ref->referral_code)); $content .= '
You have %d new referral%s today.
', $count, $count !== 1 ? 's' : '' ); $content .= '| Referred By | '; $content .= 'New User | '; $content .= 'Status | '; $content .= 'Time | '; $content .= '|
|---|---|---|---|---|
| %s | ', esc_html($referral->referrer_name ?? 'Unknown')); $content .= sprintf('%s | ', esc_html($referral->referee_name)); $content .= sprintf('%s | ', esc_html($referral->referee_email)); $content .= sprintf('%s | ', esc_html(ucfirst($referral->status))); $content .= sprintf('%s | ', esc_html(date('g:i A', strtotime($referral->referred_at)))); $content .= '
See attached CSV for full details.
'; return jvbGetEmailTemplate($content, 'Daily Referral Report'); } /** * Generate HTML email for weekly report * * @param array $top_referrers * @param int $total_referrals * @return string */ protected function generateWeeklyReportEmail(array $top_referrers, int $total_referrals): string { $content = sprintf( 'This week you had %d total referral%s.
', $total_referrals, $total_referrals !== 1 ? 's' : '' ); $content .= '| Rank | '; $content .= 'User | '; $content .= 'Total Referrals | '; $content .= 'Treated | '; $content .= '
|---|---|---|---|
| %d | ', $rank++); $content .= sprintf('%s | ', esc_html($referrer->user_name)); $content .= sprintf('%d | ', $referrer->referral_count); $content .= sprintf('%d | ', $referrer->treated_count); $content .= '
|
Users can sign up with this code. Share link: |
| Total Referrals: | |
|---|---|
| Treated: | |
| Pending: | |
| Available Rewards: | $ |
| Redeemed Rewards: | $ |
| Name | Status | Referred At | Actions | |
|---|---|---|---|---|
| referee_name); ?> | referee_email); ?> | status)); ?> | referred_at); ?> | status === 'pending'): ?> |
We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!
Can\'t find it? Check your spam folder.
We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.
Can\'t find it? Check your spam folder.
You'll receive:
your first treatment!
Your referral code has been applied. Book your free consultation now to claim your reward!
Book Your Free ConsultationInvite your friends.
Earn rewards when they book!
= esc_url($share_url)?>
=esc_html($referral_code)?>
Since you were referred by a friend, you\'ve earned ' . $reward_text . ' your first booking!
'; $bonus_content .= 'Your reward will be automatically applied when you book.
'; $bonus_content .= '