'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 = CacheManager::for('referrals', WEEK_IN_SECONDS); $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(); // Register admin subpage add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1); // Add admin bar label for referral page add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999); // Add admin notice to referral page edit screen add_action('admin_notices', [$this, 'showReferralPageNotice']); add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 2); // Handle settings save add_action('admin_init', [$this, 'registerSettings']); } 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 = '

Daily Referral Report

'; $content .= '

' . count($new_referrals) . ' new referral' . (count($new_referrals) !== 1 ? 's' : '') . ' yesterday (' . $yesterday . ')

'; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; foreach ($new_referrals as $ref) { $content .= ''; $content .= sprintf('', esc_html($ref->referee_name)); $content .= sprintf('', esc_html($ref->referee_email)); $content .= sprintf('', esc_html($ref->referrer_name)); $content .= sprintf('', esc_html($ref->referral_code)); $content .= ''; } $content .= '
RefereeEmailReferrerCode
%s%s%s%s
'; // Get admin email $to = get_option('admin_email'); $subject = sprintf('[%s] %d New Referral%s', get_bloginfo('name'), count($new_referrals), count($new_referrals) !== 1 ? 's' : ''); jvbMail($to, $subject, $content); } /** * Send weekly report with top referrers */ public function sendWeeklyReport(): void { $top_referrers = $this->getTopReferrers(10, 'week'); $total_referrals = $this->wpdb->get_var( "SELECT COUNT(*) FROM {$this->referrals_table} WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)" ); if ($total_referrals == 0) { return; } $to = get_option('admin_email'); $subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y'); $message = $this->generateWeeklyReportEmail($top_referrers, $total_referrals); wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8']); } /** * Generate CSV content from referrals * * @param array $referrals * @return string */ protected function generateCSV(array $referrals): string { $csv = "Referred By,Referee Name,Referee Email,Referee Phone,Referral Code,Status,Referred At,Treated At\n"; foreach ($referrals as $referral) { $csv .= sprintf( '"%s","%s","%s","%s","%s","%s","%s","%s"' . "\n", $referral->referrer_name ?? 'Unknown', $referral->referee_name, $referral->referee_email, $referral->referee_phone, $referral->referral_code, $referral->status, $referral->referred_at, $referral->treated_at ?? 'Not yet' ); } return $csv; } /** * Generate HTML email for daily report * * @param array $referrals * @param string $period * @return string */ protected function generateReportEmail(array $referrals, string $period): string { $count = count($referrals); $content = sprintf('

You have %d new referral%s today.

', $count, $count !== 1 ? 's' : '' ); $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; foreach ($referrals as $referral) { $content .= ''; $content .= sprintf('', esc_html($referral->referrer_name ?? 'Unknown')); $content .= sprintf('', esc_html($referral->referee_name)); $content .= sprintf('', esc_html($referral->referee_email)); $content .= sprintf('', esc_html(ucfirst($referral->status))); $content .= sprintf('', esc_html(date('g:i A', strtotime($referral->referred_at)))); $content .= ''; } $content .= '
Referred ByNew UserEmailStatusTime
%s%s%s%s%s
'; $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 .= '

Top 10 Referrers This Week

'; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $content .= ''; $rank = 1; foreach ($top_referrers as $referrer) { $content .= ''; $content .= sprintf('', $rank++); $content .= sprintf('', esc_html($referrer->user_name)); $content .= sprintf('', $referrer->referral_count); $content .= sprintf('', $referrer->treated_count); $content .= ''; } $content .= '
RankUserTotal ReferralsTreated
%d%s%d%d
'; return jvbGetEmailTemplate($content, 'Weekly Referral Summary'); } /** * Get reward settings * * @return array */ public function getRewardSettings(): array { $saved = get_option(BASE . 'referral_settings', []); return wp_parse_args($saved, $this->default_settings); } /** * Display referral info in user profile * * @param WP_User $user */ public function displayUserReferralInfo(WP_User $user): void { if (!current_user_can('edit_user', $user->ID)) { return; } $referral_code = get_user_meta($user->ID, BASE . 'referral_code', true); $stats = $this->getUserStats($user->ID); $referrals = $this->getUserReferrals($user->ID, ['limit' => 10]); ?>

Referral Information

Users can sign up with this code. Share link:

Referral Statistics

Total Referrals:
Treated:
Pending:
Available Rewards: $
Redeemed Rewards: $

Recent Referrals

Name Email Status Referred At Actions
referee_name); ?> referee_email); ?> status)); ?> referred_at); ?> status === 'pending'): ?>
add('invalid_referral_code', 'Referral code can only contain letters and numbers.'); }); return; } // Check if code is unique if ($this->isCodeTaken($code, $user_id)) { add_action('user_profile_update_errors', function($errors) { $errors->add('referral_code_taken', 'This referral code is already in use.'); }); return; } update_user_meta($user_id, BASE . 'referral_code', strtoupper($code)); } /** * Display user's referral code and share options * Use in templates or dashboard with: echo jvbReferralShareWidget(); * * @return string HTML output */ public function outputShareWidget(array $actions):array { $user_id = get_current_user_id(); $content = ''; $actions[] =[ 'button' => '', 'content' => $content ]; return $actions; } function getUnloggedInReferral(): string { ob_start(); JVB()->connect('cloudflare')->renderTurnstile(); $turnstile = ob_get_clean(); $meta = new MetaForm(); $codeForm = '
'.jvbFormStatus().$meta->return('referral_name', null, [ 'required' => true, 'type' => 'text', 'label' => 'Your Name', 'placeholder'=> 'Mister Meeseeks', 'autocomplete'=>'name' ]). $meta->return('referral_email', null, [ 'required' => true, 'type' => 'email', 'label' => 'Your Email', 'placeholder'=> 'look@me.com', 'autocomplete'=> 'email' ]). $meta->return('referral_code', null, [ 'required' => true, 'type' => 'text', 'label' => 'Referral Code', 'pattern' => '[A-Za-z0-9]+', 'maxLength' => 20, 'autocomplete'=>'off' ]).'

We\'ll send you a link to complete your registration.

'.$turnstile.'
'; $loginForm = '
'.jvbFormStatus().$meta->return('login_email', null, [ 'required' => true, 'type' => 'email', 'label' => 'Your Email', 'autocomplete'=>'email' ]).' '.$turnstile.'
'; $tabs = [ 'enterCode' => [ 'title' => 'Have a Code?', 'description' => [ 'Enter the code given to you to get 20% off your first treatment!' ], 'content' => $codeForm ], 'login' => [ 'title' => 'Login', 'description' => [ 'Login to see your rewards' ], 'content' => $loginForm ] ]; return jvbRenderTabs($tabs, true); } protected function getReferralSuccessMessage(string $code): string { $referrer = $this->getUserByReferralCode($code); if (!$referrer) { return ''; } $settings = $this->getRewardSettings(); $reward_amount = $settings['referee_reward_amount'] ?? 20; $reward_type = $settings['referee_reward_type'] ?? 'percentage'; $reward_text = $reward_type === 'percentage' ? $reward_amount . '% off' : '$' . number_format($reward_amount, 2) . ' off'; $booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact')); ob_start(); ?>

Success! Your Reward is Ready!

You'll receive:

your first treatment!

Your referral code has been applied. Book your free consultation now to claim your reward!

Book Your Free Consultation
Referred by display_name); ?>
getUserReferralCode($user_id); if (is_wp_error($referral_code)) { return ''; } } $share_url = home_url('/?ref=' . $referral_code); ob_start(); ?>

Share the ♡

Invite your friends.

Earn rewards when they book!

20]); ?> Email 20]); ?> Facebook 20]); ?> Twitter

Your Referral Link

Your Code

- Total Referrals
- Successful
- Pending
$0.00 Available Rewards
getReferralByReferee($user->ID); if (!$referral) { return $content; } $settings = get_option(BASE . 'referral_settings', []); $reward_amount = $settings['referee_reward_amount'] ?? 20; $reward_type = $settings['referee_reward_type'] ?? 'percentage'; $reward_text = $reward_type === 'percentage' ? $reward_amount . '% off' : '$' . number_format($reward_amount, 2) . ' off'; $bonus_content = '
'; $bonus_content .= '

🎉 Welcome Bonus!

'; $bonus_content .= '

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 .= '
'; // Insert bonus content after the first paragraph $parts = explode('

', $content, 2); if (count($parts) === 2) { return $parts[0] . '

' . $bonus_content . $parts[1]; } return $content . $bonus_content; } /** * Send referral invitation via email with magic link * * @param int $user_id Referrer's user ID * @param string $invitee_email Email of person to invite * @param string $invitee_name Name of person to invite * @return array|WP_Error Result with success/error */ public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name):array|WP_Error { // Verify user exists if (!$this->checkUser($user_id)) { return new WP_Error('invalid_user', 'Invalid user ID'); } // Check email rate limit (15/hour) $rate_check = $this->checkEmailRateLimit($user_id); if ($rate_check !== true) { return new WP_Error('rate_limit', 'You can only send 15 invitations per hour. Please try again later.'); } // Validate email $invitee_email = sanitize_email($invitee_email); if (!is_email($invitee_email)) { return new WP_Error('invalid_email', 'Invalid email address'); } // Check if this email has already been invited or registered if ($this->isEmailInvited($invitee_email)) { return new WP_Error('already_invited', 'This person has already been invited'); } if (email_exists($invitee_email)) { return new WP_Error('user_exists', 'This person already has an account'); } // Get referrer info $referrer = get_user_by('ID', $user_id); $referral_code = $this->getUserReferralCode($user_id); if (is_wp_error($referral_code)) { return $referral_code; } // Get reward text for email $settings = $this->getRewardSettings(); $reward_text = $settings['referee_reward_type'] === 'percentage' ? "Get {$settings['referee_reward_amount']}% off your first treatment!" : "Get \${$settings['referee_reward_amount']} off your first treatment!"; // Record the invitation attempt (for tracking) $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name); // Send magic link via MagicLinkManager $result = $this->magic_link->sendMagicLink( $invitee_email, MagicLinkManager::TYPE_REFERRAL, [ 'name' => sanitize_text_field($invitee_name), 'referral_code' => $referral_code, 'referrer_id' => $user_id, 'referrer_name' => $referrer->display_name, 'reward_text' => $reward_text ] ); if (is_wp_error($result)) { return $result; } return [ 'success' => true, 'message' => 'Invitation sent successfully', 'email' => $invitee_email, 'name' => $invitee_name ]; } /** * Send multiple referral invitations * * @param int $user_id Referrer's user ID * @param array $invitations Array of ['email' => '', 'name' => ''] * @return array Results with success/failed arrays */ public function sendBatchReferralInvitations(int $user_id, array $invitations): array { $results = [ 'success' => [], 'failed' => [] ]; foreach ($invitations as $invite) { $email = $invite['email'] ?? ''; $name = $invite['name'] ?? ''; if (empty($email) || empty($name)) { $results['failed'][] = [ 'email' => $email, 'name' => $name, 'reason' => 'Missing email or name' ]; continue; } $result = $this->sendReferralInvitation($user_id, $email, $name); if (is_wp_error($result)) { $results['failed'][] = [ 'email' => $email, 'name' => $name, 'reason' => $result->get_error_message() ]; } else { $results['success'][] = [ 'email' => $email, 'name' => $name ]; } // Small delay between sends to be respectful usleep(100000); // 0.1 seconds } return [ 'success' => !empty($results['success']), 'results' => $results, 'summary' => sprintf( 'Sent %d invitations, %d failed', count($results['success']), count($results['failed']) ) ]; } /** * Check email invitation rate limit (15 per hour) * * @param int $user_id * @return true|string True if allowed, error message if limited */ protected function checkEmailRateLimit(int $user_id):bool|string { $hourly_key = 'referral_invites_hour_' . $user_id; $count = (int) get_transient($hourly_key); if ($count >= 15) { return 'hourly_limit_reached'; } set_transient($hourly_key, $count + 1, HOUR_IN_SECONDS); return true; } /** * Check if an email has already been invited * * @param string $email * @return bool */ protected function isEmailInvited(string $email): bool { // Check invitation tracking table $invitation_key = 'referral_invite_' . md5($email); $invited = get_transient($invitation_key); if ($invited) { return true; } // Check if there's a pending referral for this email $existing = $this->wpdb->get_var($this->wpdb->prepare( "SELECT id FROM {$this->referrals_table} WHERE referee_email = %s", $email )); return !empty($existing); } /** * Record invitation attempt (for tracking and preventing duplicates) * * @param int $user_id * @param string $email * @param string $name */ protected function recordInvitationAttempt(int $user_id, string $email, string $name): void { // Store for 30 days (same as magic link invitation validity) $invitation_key = 'referral_invite_' . md5($email); $data = [ 'inviter_id' => $user_id, 'email' => $email, 'name' => $name, 'sent_at' => current_time('mysql') ]; set_transient($invitation_key, $data, 30 * DAY_IN_SECONDS); // Also log in user meta for tracking $sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: []; $sent_invites[] = [ 'email' => $email, 'name' => $name, 'sent_at' => current_time('mysql') ]; update_user_meta($user_id, BASE . 'referral_invites_sent', $sent_invites); } /** * Get user's invitation stats * * @param int $user_id * @return array */ public function getUserInvitationStats(int $user_id): array { $sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: []; // Count invites sent in last hour $one_hour_ago = strtotime('-1 hour'); $recent_count = 0; foreach ($sent_invites as $invite) { if (strtotime($invite['sent_at']) > $one_hour_ago) { $recent_count++; } } return [ 'total_sent' => count($sent_invites), 'sent_last_hour' => $recent_count, 'remaining_this_hour' => max(0, 15 - $recent_count), 'can_send_more' => $recent_count < 15 ]; } /** * Export referrals for Jane App cross-reference * * @param string $start_date Y-m-d format * @param string $end_date Y-m-d format * @return string CSV content */ public function exportReferrals(string $start_date, string $end_date): string { $referrals = $this->wpdb->get_results($this->wpdb->prepare( "SELECT r.id, r.referee_name, r.referee_email, r.referee_phone, r.referral_code, r.referred_at, r.status, r.treated_at, u.display_name as referrer_name, u.user_email as referrer_email FROM {$this->referrals_table} r JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID WHERE DATE(r.referred_at) BETWEEN %s AND %s ORDER BY r.referred_at DESC", $start_date, $end_date )); // Build CSV $csv_lines = []; // Headers $csv_lines[] = [ 'Referral ID', 'Referee Name', 'Referee Email', 'Referee Phone', 'Referral Code', 'Referred Date', 'Status', 'Treated Date', 'Referrer Name', 'Referrer Email' ]; // Data rows foreach ($referrals as $ref) { $csv_lines[] = [ $ref->id, $ref->referee_name, $ref->referee_email, $ref->referee_phone ?: 'N/A', $ref->referral_code, $ref->referred_at, ucfirst($ref->status), $ref->treated_at ?: 'N/A', $ref->referrer_name, $ref->referrer_email ]; } // Convert to CSV string $output = fopen('php://temp', 'r+'); foreach ($csv_lines as $line) { fputcsv($output, $line); } rewind($output); $csv_content = stream_get_contents($output); fclose($output); return $csv_content; } /** * Add referral settings subpage to admin menu * * @param array $subpages * @return array */ public function addSubpage(array $subpages): array { $subpages[] = [ 'page_title' => 'Referral Settings', 'menu_title' => 'Referrals', 'capability' => 'manage_options', 'menu_slug' => 'jvb-referrals', 'callback' => [$this, 'renderAdminPage'], 'icon' => 'users', ]; return $subpages; } /** * Register settings */ public function registerSettings(): void { register_setting( BASE . 'referral_settings', BASE . 'referral_page_id', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 0 ] ); register_setting( BASE . 'referral_settings', BASE . 'referral_reward_settings', [ 'type' => 'array', 'sanitize_callback' => [$this, 'sanitizeRewardSettings'], 'default' => $this->default_settings ] ); } /** * Sanitize reward settings */ public function sanitizeRewardSettings(array $settings): array { return [ 'referrer_reward_applies_to' => in_array($settings['referrer_reward_applies_to'] ?? '', ['per_user', 'flat_total']) ? $settings['referrer_reward_applies_to'] : 'per_user', 'referrer_reward_amount' => floatval($settings['referrer_reward_amount'] ?? 25.00), 'referrer_reward_type' => in_array($settings['referrer_reward_type'] ?? '', ['fixed', 'percentage']) ? $settings['referrer_reward_type'] : 'fixed', 'referee_reward_type' => in_array($settings['referee_reward_type'] ?? '', ['percentage', 'fixed']) ? $settings['referee_reward_type'] : 'percentage', 'referee_reward_amount' => floatval($settings['referee_reward_amount'] ?? 20), 'referee_reward_applies_to' => in_array($settings['referee_reward_applies_to'] ?? '', ['first_order', 'all_orders']) ? $settings['referee_reward_applies_to'] : 'first_order', ]; } /** * Render the admin settings page */ public function renderAdminPage(): void { // Handle form submission if (isset($_POST['submit']) && check_admin_referer(BASE . 'referral_settings_nonce')) { update_option(BASE . 'referral_page_id', absint($_POST[BASE . 'referral_page_id'] ?? 0)); $reward_settings = [ 'referrer_reward_applies_to' => sanitize_text_field($_POST['referrer_reward_applies_to'] ?? 'per_user'), 'referrer_reward_amount' => floatval($_POST['referrer_reward_amount'] ?? 25.00), 'referrer_reward_type' => sanitize_text_field($_POST['referrer_reward_type'] ?? 'fixed'), 'referee_reward_type' => sanitize_text_field($_POST['referee_reward_type'] ?? 'percentage'), 'referee_reward_amount' => floatval($_POST['referee_reward_amount'] ?? 20), 'referee_reward_applies_to' => sanitize_text_field($_POST['referee_reward_applies_to'] ?? 'first_order'), ]; update_option(BASE . 'referral_reward_settings', $this->sanitizeRewardSettings($reward_settings)); echo '

Settings saved successfully.

'; } $referral_page_id = $this->getReferralPageId(); $settings = $this->getRewardSettings(); echo $this->renderAdminHTML(); } protected function renderAdminHTML():string { ob_start(); ?>

Referral Settings

Referral Page

Select the page where users can access their referral dashboard.

BASE . 'referral_page_id', 'id' => BASE . 'referral_page_id', 'selected' => $referral_page_id, 'show_option_none' => __('— Select —', 'jvbase'), 'option_none_value' => '0' ]); ?>

This page will show "Referral Page" in the admin bar when editing.

Reward Settings

Referrer Rewards

Amount in dollars or percentage

Referee (New User) Rewards

Amount in dollars or percentage

renderReferralStats(true) ?>
get_var("SELECT COUNT(*) FROM {$this->referrals_table}"); $pending_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'pending'"); $treated_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'treated'"); ?>
Total Referrals
Pending
Treated

Referral Statistics

'.$table.' '; } return $table; } /** * Add "Referral Page" label to admin bar * * @param WP_Admin_Bar $wp_admin_bar */ public function addReferralPageLabel($wp_admin_bar): void { if (!is_admin()) { return; } $referral_page_id = $this->getReferralPageId(); if (!$referral_page_id) { return; } global $pagenow, $post; // Check if we're editing the referral page if ('post.php' === $pagenow && $post && $post->ID === $referral_page_id) { $wp_admin_bar->add_node([ 'id' => 'referral-page', 'parent' => 'top-secondary', 'title' => __('Referral Page', 'jvbase'), 'meta' => [ 'class' => 'referral-page-notice' ] ]); } } /** * Get the referral page ID * * @return int|null */ public function getReferralPageId(): ?int { $page_id = get_option(BASE . 'referral_page_id'); return $page_id ? (int) $page_id : null; } /** * Show admin notice on referral page edit screen */ public function showReferralPageNotice(): void { global $pagenow, $post; if ('post.php' !== $pagenow || !$post) { return; } $referral_page_id = $this->getReferralPageId(); if ($post->ID === $referral_page_id) { echo '
'; echo '

' . __('This page is designated as the Referral Page.', 'jvbase') . '

'; echo '
'; } } public function renderDashPage(string $content, string $page):string { if ($page !== 'referrals') { return $content; } $out = ''; if (current_user_can('manage_options')) { $out .= $this->renderAdminHTML(); } else { $out .= $this->renderReferralStats(true); } return ($out === '') ? $content : '
'.$out.'
'; } }