'per_user', // 'per_user' or 'flat_total' 'from_user_reward_amount' => 25.00, 'from_user_reward_type' => 'fixed', 'to_user_reward_type' => 'percentage', // 'percentage' or 'fixed' 'to_user_reward_amount' => 20, // 20% or $20 'to_user_reward_applies_to' => 'first_order', // 'first_order' or 'all_orders' ]; protected string $role; protected array $settings; private function __construct() { $this->route = ReferralRoutes::getInstance(); $this->defineTables(); $this->role = Site::getReferralRole(); $this->default_settings['referral_role'] = $this->role; $this->cache = Cache::for('referrals'); $this->statsCache = Cache::for('referral_stats', null, WEEK_IN_SECONDS)->user(); $this->referralPage = $this->getReferralPageId(); $this->settings = $this->getRewardSettings(); add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3); add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1); 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('jvbNewUserEmail', [$this, 'addReferralToWelcomeEmail'], 99, 2); add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']); add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); // Schedule cron jobs for reports $this->registerCronJobs(); // Add admin notice to referral page edit screen add_action('admin_notices', [$this, 'showReferralPageNotice']); add_action(BASE.'dashboard_page_referrals', [$this, 'renderDashPage'], 10); // add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 2); // Handle settings save add_action('admin_init', [$this, 'registerSettings']); // Handle admin page form submission add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3); add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); } protected function defineTables():void { $this->defineReferralsTable(); $this->defineCodeTable(); $this->defineJaneClientsTable(); $this->defineRewardsTable(); $this->defineTreatmentsTable(); } protected function defineReferralsTable():void { $table = CustomTable::for('referrals'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'from_user' => "{$table->getUserIDType()} NOT NULL", //The person doing the referring 'to_user' => "{$table->getUserIDType()} DEFAULT NULL", //The person being referred 'to_name' => 'varchar(255) DEFAULT NULL', 'to_email' => 'varchar(255) DEFAULT NULL', 'to_phone' => 'varchar(50) DEFAULT NULL', 'referral_code'=> 'varchar(50) NOT NULL', 'status' => "ENUM('invited', 'pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'", 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', 'last_invited'=> 'datetime DEFAULT CURRENT_TIMESTAMP', 'consulted_at'=> 'datetime DEFAULT NULL', 'treated_at'=> 'datetime DEFAULT NULL', 'treatment_count' => 'int DEFAULT 0', 'notes' => 'text DEFAULT NULL', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => 'id'], // ['key' => 'UNIQUE', 'value' => '(`to_user`)'], 'from_user (`from_user`)', 'to_user (`to_user`)', 'status (`status`)', 'code (`referral_code`)', 'date (`created_at`)', 'consult (`consulted_at`)' ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}referral_from_user_fk` FOREIGN KEY (`from_user`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", "CONSTRAINT `{$base}referral_to_user_fk` FOREIGN KEY (`to_user`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->referrals = $table; } protected function defineCodeTable():void { $table = CustomTable::for('referrals_codes'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'code' => 'varchar(50) NOT NULL', 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], ['key' => 'UNIQUE', 'value' => '(`code`)'], 'user (`user_id`)', ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}referral_code_user_fk` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", ]); $table->defineTable(); $this->codes = $table; } protected function defineJaneClientsTable():void { $table = CustomTable::for('referrals_jane_clients'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'patient_guid' => 'varchar(50) NOT NULL', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'first_name' => 'varchar(100) NOT NULL', 'last_name' => 'varchar(100) NOT NULL', 'email' => 'varchar(255) NOT NULL', 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], ['key' => 'UNIQUE', 'value' => '(`patient_guid`)'], 'user (`user_id`)', 'email (`email`)', ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}jane_clients_user` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", ]); $table->defineTable(); $this->janeClients = $table; } protected function defineRewardsTable():void { $table = CustomTable::for('referrals_rewards'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'referral_id' => 'bigint(20) unsigned NOT NULL', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'reward_type' => "ENUM('from_user', 'to_user') NOT NULL", 'amount' => 'decimal(10,2) NOT NULL', 'reward_calculation'=> "ENUM('percentage', 'fixed')", 'status' => "ENUM('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available'", 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'redeemed_at' => 'datetime DEFAULT NULL', 'expires_at' => 'datetime DEFAULT NULL', 'notes' => 'text DEFAULT NULL', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], 'referral (`referral_id`)', 'user (`user_id`)', 'status (`status`)', 'type (`reward_type`)' ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}reward_referral` FOREIGN KEY (`referral_id`) REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE", "CONSTRAINT `{$base}reward_user` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->rewards = $table; } protected function defineTreatmentsTable():void { $table = CustomTable::for('referral_treatments'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'referral_id' => 'bigint(20) unsigned NOT NULL', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'treatment_type'=> 'varchar(100) NOT NULL', //Tier 1-6, Brows, etc 'treatment_date'=> 'datetime NOT NULL', 'invoice_number'=> 'varchar(50) DEFAULT NULL', 'amount' => 'decimal(10,2) DEFAULT NULL', 'status' => "ENUM('completed', 'no_show', 'cancelled') DEFAULT 'completed'", 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], 'referral (`referral_id`)', 'user (`user_id`)', 'date (`treatment_date`)', 'type (`treatment_type`)', ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}treatment_referral` FOREIGN KEY (`referral_id`) REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE", "CONSTRAINT `{$base}treatment_user` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->treatments = $table; } public function getSettings():array { return $this->settings; } public function getRole():string { return $this->role; } public function addLoginInputs(string $action):void { if (array_key_exists('ref', $_GET)) { echo ''; } } public function modifyLoginText(array $defaults):array { if (!array_key_exists('ref', $_GET)){ return $defaults; } $code = $_GET['ref']; $user = $this->getUserByReferralCode($code); $desc = ($user) ? strtok($user->display_name, ' ') . ' invited you ' : 'You\'ve been invited '; $defaults['title'] = 'Register your Account. Get Your Reward.'; $defaults['description'] = [ $desc.' to see the difference with us.', 'Oh, and you\'ll get 20% off your first treatment!', 'Finish this account creation (you\'ll still need to make an account on Jane App to book), or let us know when you come in for your appointment.' ]; $defaults['extra'] = [ 'Once you\'re in, you\'ll get your own code you can share.', 'Get a $25 credit for each person you send our way who comes in for their first treatment!' ]; return $defaults; } public function enqueueScripts():void { $requirements = [ 'jvb-utility', 'jvb-a11y', 'jvb-popup', 'jvb-tabs', 'jvb-data-store', ]; if (Site::hasIntegration('cloudflare') && Cloudflare::getInstance()->isSetUp()) { Cloudflare::getInstance()->enqueueTurnstileScripts(); } if (is_singular(BASE.'dash')) { $requirements[] = 'jvb-form'; $requirements[] = 'jvb-view'; wp_enqueue_script('jvb-referral-admin', JVB_URL.'assets/js/min/referralAdmin.min.js', ['jvb-referral'], '1.0.0', true); } 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'); } } public function createCode(int $user_id, string $code):string|false { $code = sanitize_title($code); $existing = $this->codes->get(['code' => $code]); if ($existing) { if ($existing['user_id'] !== $user_id) { return false; } return $code; } $success = $this->codes->insert([ 'user_id' => $user_id, 'code' => $code ]); if ($success) { return $code; } return false; } /** * 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):string|false { $user = get_userdata($user_id); if (!$user) { return false; } $existing = $this->codes->pluck('code', ['user_id' => $user_id],'created_at', 'DESC'); if (!empty($existing) && !$custom_code) { return $existing[0]; } if ($custom_code && !empty($existing) && !in_array($custom_code, $existing)) { $test = $this->createCode($user_id, $custom_code); if ($test) { return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC')[0]; } else { return $existing[0]; } } // Generate new code if custom provided or none exists $code = $this->generateReferralCode($user); $success = $this->createCode($user_id, $code); if ($success) { return $success; } return false; } /** * 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 * @return bool */ protected function isCodeTaken(string $code): bool { return (bool) $this->codes->get(['code' => $code]); } public function processRegistrationToken(int $user_id, string $email, array $data): void { // Check for referral code in data $code = $data['referral_code'] ?? ''; if (empty($code)) { return; } // Store in session/cookie for processReferral to pick up if (session_status() === PHP_SESSION_NONE) { session_start(); } $_SESSION[BASE . 'referral_code'] = sanitize_text_field($code); setcookie( BASE . 'referral_code', sanitize_text_field($code), time() + (86400 * 30), '/' ); } /** * Track a new referral when user registers * * @param int $user_id * @param array $userData * @return void */ public function processReferral(int $user_id, array $userData):void { if (!array_key_exists('ref', $_POST) || empty($_POST['ref'])) { return; } $code = sanitize_text_field($_POST['ref']); $referredBy = $this->codes->pluck('user_id', ['code' => $code]); if (!empty($referredBy)) { $this->createReferralFromID($referredBy[0], $user_id, $code); } } /** * Create a referral record in the database * * @param int $from_user_id The person sending the referral * @param string $name The person who was referred * @param string $email The person who was referred's email * @param ?string $phone The person who was referred's phone (optional) * @param ?string $code The person sending the referral's custom code * @return array */ public function createReferralFromFields(int $from_user_id, string $name, string $email, ?string $phone = null, ?string $code = null):array { if (!get_userdata($from_user_id)) { return [ 'success' => false, 'message' => 'From user does not exist', ]; } if (filter_var($email, FILTER_VALIDATE_EMAIL)) { if(email_exists($email)) { return [ 'success' => false, 'message' => 'Email already exists', ]; } } if ($phone && !empty($this->referrals->pluck('to_phone', ['to_phone' => Sanitizer::sanitizePhone($phone)]))) { return [ 'success' => false, 'message' => 'Phone already used' ]; } $insert = [ 'from_user' => $from_user_id, 'referral_code' => is_null($code)? $this->getUserReferralCode($from_user_id) : sanitize_text_field($code), 'to_name' => $name, 'to_email' => $email, 'status' => 'invited', ]; if ($phone) { $insert['to_phone'] = $phone; } $created = $this->referrals->insert($insert); $this->statsCache->forget($from_user_id); return [ 'success' => (bool) $created, 'message' => 'Referral created' ]; } /** * Create a referral record in the database * * @param int $from_user_id The person sending the referral * @param int $to_user_id The person who was referred * @param ?string $code Custom code, if not default * @return array */ public function createReferralFromID(int $from_user_id, int $to_user_id, ?string $code = null):array { $user = get_userdata($to_user_id); $code = !is_null($code) ? sanitize_text_field($code) : $this->getUserReferralCode($to_user_id); $where = [ 'to_email' => $user->user_email, 'to_name' => $user->first_name ?: $user->display_name ]; $phone = false; if (array_key_exists('phone', $_POST) && !empty($_POST['phone'])) { $phone = Sanitizer::sanitizePhone($_POST['phone']); if (!empty($phone)) { $where = [ 'to_phone' => $phone, ]; } } elseif (!empty(Meta::forUser($to_user_id)->get('phone'))) { $phone = Meta::forUser($to_user_id)->get('phone'); } $where = [ 'OR' => $where, 'status' => 'invited' ]; $update = [ 'from_user' => $from_user_id, 'to_user' => $to_user_id, 'to_name' => $user->first_name ?: $user->display_name, 'to_email' => $user->user_email, 'status' => 'pending', 'referral_code' => $code ]; if ($phone) { $update ['to_phone'] = $phone; } $existing = $this->referrals->get($where); if ($existing) { if ($existing->from_user != $from_user_id) { $this->referrals->update([ 'status' => 'cancelled', ], $where); } else { $updated = $this->referrals->update($update, $where); $this->sendReferrerNotification($from_user_id, $update['to_name']); return [ 'success' => (bool) $updated, 'message' => $updated ? 'Successfully created referral' : 'Referral could not be updated' ]; } } $created = $this->referrals->insert($update); $this->statsCache->forget($from_user_id); return [ 'success' => (bool) $created, 'message' => $created ? 'Successfully created referral' : 'Referral could not be added' ]; } /** * Get user by referral code * * @param string $code * @return WP_User|null */ public function getUserByReferralCode(string $code): ?WP_User { return $this->cache->remember( $code, function () use ($code) { $users = get_users([ 'meta_key' => BASE . 'referral_code', 'meta_value' => $code, 'number' => 1 ]); return $users[0] ?? null; } ); } /** * Get referral record by to_user ID * * @param int $to_user_id * @return object|null */ public function getReferralByReferee(int $to_user_id): ?object { $result = $this->referrals->get([ 'to_user' => $to_user_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->referrals->update( [ 'status' => $status, 'treated_at' => $treated ? current_time('mysql') : null ], [ 'id' => $referral_id ] ); if ($result && $treated) { // Create reward records when marking as treated $this->createRewardRecords($referral_id); } return $result !== false; } /** * Create reward records for both from_user and to_user * * @param int $referral_id */ protected function createRewardRecords(int $referral_id): void { $referral = $this->referrals->get(['id' => $referral_id]); if (!$referral) { return; } // Create from_user reward $fromUserReward = $this->rewards->insert([ 'referral_id' => $referral_id, 'user_id' => $referral->from_user, 'reward_type' => 'from_user', 'amount' => $this->settings['from_user_reward_amount'], 'reward_calculation' => $this->settings['from_user_reward_type'] ]); // Create to_user reward $to_user_amount = $this->settings['to_user_reward_type'] === 'percentage' ? $this->settings['to_user_reward_amount'] // Store as percentage : $this->settings['to_user_reward_amount']; // Store as fixed amount $toUserReward = $this->rewards->insert([ 'referral_id' => $referral_id, 'user_id' => $referral->to_user, 'reward_type' => 'to_user', 'amount' => $to_user_amount, 'reward_calculation' => $this->settings['to_user_reward_type'] ]); } /** * 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', 'per_page' => 100, 'page' => 1, 'orderby' => 'created_at', 'order' => 'DESC' ]; $where = [ 'from_user' => $user_id, ]; foreach ($this->referrals->getColumnKeys() as $col) { if (array_key_exists($col, $args)) { if ($col === 'status' && $args[$col] !== 'all') { $where[$col] = $args[$col]; } else { $where[$col] = $args[$col]; } } } $args = wp_parse_args($args, $defaults); $args['status'] = strtolower($args['status']); $tableArgs = [ 'where' => $where, 'per_page' => $args['per_page'], 'page' => $args['page'], 'orderby' => $args['orderby'], 'order' => $args['order'] ]; if ($args['status'] !== 'all'){ $tableArgs['status'] = $args['status']; } $referrals = $this->referrals->getMany($tableArgs, false); return array_map(function($referral) { $can_resend = $this->checkCanResendEmail($referral->id); $status = match($referral->status) { 'consulted' => 'Awaiting Treatment', 'treated' => 'Rewarded!', 'invited' => 'Invited, but has not used', default => 'Pending', }; return [ 'id' => $referral->id, 'to_name' => $referral->to_name, 'to_email' => $referral->to_email, 'referred_at' => ReferralRoutes::getInstance()->formatTimestamp($referral->referred_at), 'referral_status'=> $status, 'can_resend' => $can_resend ]; }, $referrals); } protected function checkCanResendEmail(int|object $referral): bool { if (is_int($referral)) { $referral = $this->referrals->get(['id'=>$referral]); if (!$referral) { return false; } } return (time() - $referral->updated_at) > WEEK_IN_SECONDS; } /** * Get referral statistics for a user * * @param int $user_id * @return array */ public function getUserStats(int $user_id): array { return $this->statsCache->remember( $user_id, function() use ($user_id) { $stats = [ 'total' => $this->referrals->count(['user_id' => $user_id]), 'invited' => $this->referrals->count(['user_id' => $user_id, 'status' => 'invited']), 'pending' => $this->referrals->count(['user_id' => $user_id, 'status' => 'pending']), 'consulted' =>$this->referrals->count(['user_id' => $user_id, 'status' => 'consulted']), 'treated' => $this->referrals->count(['user_id' => $user_id, 'status' => 'treated']), 'total_rewards' => $this->rewards->sum('amount', ['user_id' => $user_id]), 'available_rewards' => $this->rewards->sum('amount', ['user_id' => $user_id, 'redeemed_at' => ['NOT NULL', null]]), ]; $stats['user_id'] = $user_id; return $stats; } ); } /** * Get top from_users 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 { return $this->statsCache->remember( ['per_page' => $limit, 'period' => $period], function() use ($limit, $period) { $where = ''; if ($period !== 'all') { $date_clause = match($period) { 'day' => "created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)", 'week' => "created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)", 'month' => "created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)", default => "1=1" }; $where = "WHERE {$date_clause}"; } $results = $this->referrals->queryResults( "SELECT from_user, COUNT(*) as referral_count, SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count FROM {table} {$where} GROUP BY from_user ORDER BY referral_count DESC LIMIT {$limit}" ); foreach ($results as &$result) { $user = get_user_by('ID', $result->from_user); $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')); $new_referrals = $this->referrals->queryResults( "SELECT {table}.*, u.display_name as from_user_name FROM {table} JOIN {$this->referrals->getUserTable()} u ON {table}.from_user = u.ID WHERE DATE({table}.created_at) = %s ORDER BY {table}.created_at DESC", [$yesterday] ); if (empty($new_referrals)) { return; } $content = Emails::getInstance()->h1('Daily Referral Report'); $content .= Emails::getInstance()->stat( count($new_referrals), count($new_referrals) === 1 ? 'New Referral' : 'New Referrals', 'From ' . $yesterday ); $content .= Emails::getInstance()->spacer(); $content .= Emails::getInstance()->h2('New Referrals'); // Build list of referrals foreach ($new_referrals as $ref) { $cardContent = sprintf( '
%s (%s)
', esc_html($ref->to_name), esc_html($ref->to_email) ); $cardContent .= sprintf( 'Referred by: %s | Code: %s
', Emails::getInstance()->colours['dark-200'], esc_html($ref->from_name), Emails::getInstance()->badge($ref->referral_code, 'info') ); $content .= Emails::getInstance()->card($cardContent); } $to = get_option('admin_email'); $subject = sprintf( '[%s] %d New Referral%s', get_bloginfo('name'), count($new_referrals), count($new_referrals) !== 1 ? 's' : '' ); Emails::getInstance()->sendEmail($to, $subject, $content, 'DAILY REPORT'); } /** * Send weekly report with top from_users */ public function sendWeeklyReport(): void { $total_referrals = $this->referrals->count(['created_at' => ['>=', "DATE_SUB(NOW(), INTERVAL 1 WEEK)"]]); if ($total_referrals == 0) { return; } $top_from_users = $this->getTopReferrers(10, 'week'); $content = Emails::getInstance()->h1('Weekly Referral Summary'); $content .= Emails::getInstance()->stat( $total_referrals, 'Total Referrals', 'This week' ); $content .= Emails::getInstance()->spacer(30); $content .= Emails::getInstance()->h2('Top 10 Referrers'); // Leaderboard style $rank = 1; foreach ($top_from_users as $from_user) { $rankBadge = $rank <= 3 ? Emails::getInstance()->badge('#' . $rank, $rank === 1 ? 'success' : 'info') : '#' . $rank . ''; $cardContent = sprintf( '%s %s
', $rankBadge, esc_html($from_user->user_name) ); $stats = [ Emails::getInstance()->stat($from_user->referral_count, 'Total Referrals'), Emails::getInstance()->stat($from_user->treated_count, 'Treated') ]; $cardContent .= Emails::getInstance()->grid($stats); $content .= Emails::getInstance()->card($cardContent); $rank++; } $to = get_option('admin_email'); $subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y'); Emails::getInstance()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY'); } /** * Generate CSV content from referrals * * @param array $referrals * @return string */ protected function generateCSV(array $referrals): string { $cache = Cache::for('referralCSV'); return $cache->remember( 'csv', function () use ($referrals) { $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->from_name ?? 'Unknown', $referral->to_name, $referral->to_email, $referral->to_phone, $referral->referral_code, $referral->status, $referral->referred_at, $referral->treated_at ?? 'Not yet' ); } return $csv; } ); } /** * Generate HTML email for weekly report * * @param array $top_from_users * @param int $total_referrals * @return string */ protected function generateWeeklyReportEmail(array $top_from_users, int $total_referrals): string { $content = sprintf( 'This week you had %d total referral%s.
', $total_referrals, $total_referrals !== 1 ? 's' : '' ); $from_users = []; $rank = 1; foreach ($top_from_users as $from_user) { $from_users[] = [ 'label' => '#' . $rank++ . ' - ' . esc_html($from_user->user_name), 'value' => sprintf( 'Total Referrals: %d | Treated: %d', $from_user->referral_count, $from_user->treated_count ) ]; } $content .= Emails::getInstance()->table($from_users, 'Top 10 Referrers This Week'); return $content; } /** * 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, ['per_page' => 10]); ?>|
Users can sign up with this code. Share link: |
| Total Referrals: | |
|---|---|
| Treated: | |
| Pending: | |
| Available Rewards: | $ |
| Redeemed Rewards: | $ |
| Name | Status | Referred At | Actions | |
|---|---|---|---|---|
| to_name); ?> | to_email); ?> | status)); ?> | referred_at); ?> | status === 'pending'): ?> |
Enter your referral code to get started!
', jvbIcon('confetti'), esc_html($reward_text), ($from_user_name ? '' . esc_html($from_user_name) . ' invited you to join us
' : '') ); $codeForm = sprintf( '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.
Already have an account?
Log in to see your rewards!
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 friends. Earn rewards.
= esc_url($share_url) ?>
Quickest and easiest: autofills your code.
= esc_html($referral_code) ?>
Manually copy and paste the code
Bulk-invite your friends via email - the link will pre-fill their name, email, and code!
Since you were referred by a friend, you\'ve earned %s your first booking!
Your reward will be automatically applied when you book.
', $reward_text, ); $code = $this->getUserReferralCode($user->ID); $yourCode = ''; if ($code) { $share_url = $this->getShareURL($code); $yourCode = sprintf( 'If you find you love what we can do for you, you can share your own code!
Your Referral Code: %s
Or click the button below:
%s%s
Click the button below to register and claim your reward:
%sThis invitation expires in 30 days.
', esc_html($from_user->display_name), esc_html(get_bloginfo('name')), nl2br(esc_html($message)), esc_html($reward_text), Emails::getInstance()->button($registration_url, 'Register & Get Your Reward') ); // Send email $sent = Emails::getInstance()->sendEmail( $invitee_email, $subject, $email_content ); if (!$sent) { return new WP_Error('email_failed', 'Failed to send invitation email'); } 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, string $subject, string $message): array { $results = [ 'success' => [], 'failed' => [] ]; $invitations = array_filter($invitations, function ($invite) { if (!array_key_exists('email', $invite) || !filter_var($invite['email'], FILTER_VALIDATE_EMAIL)) { return false; } if (!array_key_exists('name', $invite) || empty ($invite['name'])) { return false; } return true; }); foreach ($invitations as $invite) { $email = $invite['email']; $name = $invite['name']; $result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message); if (!$result['success']) { $results['failed'][] = [ 'email' => $email, 'name' => $name, 'reason' => $result['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']), 'result' => $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 { $key = BASE.'referral_email_limit'; $stored = get_user_meta($user_id, $key, true); $count = empty($stored) ? 0 : count($stored); if ($count >= 15) { return false; } $stored = empty($stored) ? [] : $stored; $stored[] = time(); $stored = array_filter($stored, function($time) { return time() - $time < HOUR_IN_SECONDS; }); update_user_meta($user_id, $key, $stored); return true; } /** * 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->referrals->queryResults( "SELECT {table}.id, {table}.to_name, {table}.to_email, {table}.to_phone, {table}.referral_code, {table}.created_at, {table}.status, {table}.treated_at, u.display_name as from_name, u.user_email as from_email FROM {table} JOIN {$this->referrals->getUserTable()} u ON {table}.from_user = u.ID WHERE DATE({table}.created_at) BETWEEN %s AND %s ORDER BY {table}.created_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->to_name, $ref->to_email, $ref->to_phone ?: 'N/A', $ref->referral_code, $ref->referred_at, ucfirst($ref->status), $ref->treated_at ?: 'N/A', $ref->from_name, $ref->from_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 * Add referral settings subpage to admin menu * * @param array $subpages * @return array */ public static function addSubpage():void { $subpage = [ 'page_title' => 'Referral System', 'menu_title' => 'Referrals', 'capability' => 'manage_options', 'menu_slug' => BASE . 'referral-admin', 'callback' => [self::class, 'renderAdminPage'] ]; AdminPages::addSubPage(BASE.'referral-admin', $subpage); } /** * 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 [ 'from_user_reward_applies_to' => in_array($settings['from_user_reward_applies_to'] ?? '', ['per_user', 'flat_total']) ? $settings['from_user_reward_applies_to'] : 'per_user', 'from_user_reward_amount' => floatval($settings['from_user_reward_amount'] ?? 25.00), 'from_user_reward_type' => in_array($settings['from_user_reward_type'] ?? '', ['fixed', 'percentage']) ? $settings['from_user_reward_type'] : 'fixed', 'to_user_reward_type' => in_array($settings['to_user_reward_type'] ?? '', ['percentage', 'fixed']) ? $settings['to_user_reward_type'] : 'percentage', 'to_user_reward_amount' => floatval($settings['to_user_reward_amount'] ?? 20), 'to_user_reward_applies_to' => in_array($settings['to_user_reward_applies_to'] ?? '', ['first_order', 'all_orders']) ? $settings['to_user_reward_applies_to'] : 'first_order', ]; } /** * Render the admin settings page */ public function renderAdminPage(): void { ?>Upload your exported CSV files from Jane App to sync client and sales data.
| %s | %s |
|---|
' . __('This page is designated as the Referral Page.', 'jvb') . '
'; echo '= home_url('/?ref=' . $referral_code) ?>
= esc_html($referral_code) ?>
People who used your code
Completed first treatment
Earned from referrals
Start sharing your referral code to earn rewards!
Share your code using the "Share" tab below.