'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 .= 'Referee ';
$content .= 'Email ';
$content .= 'Referrer ';
$content .= 'Code ';
$content .= ' ';
foreach ($new_referrals as $ref) {
$content .= '';
$content .= sprintf('%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 .= ' ';
}
$content .= '
';
// 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 .= 'Referred By ';
$content .= 'New User ';
$content .= 'Email ';
$content .= 'Status ';
$content .= 'Time ';
$content .= ' ';
foreach ($referrals as $referral) {
$content .= '';
$content .= sprintf('%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 .= ' ';
}
$content .= '
';
$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 .= 'Rank ';
$content .= 'User ';
$content .= 'Total Referrals ';
$content .= 'Treated ';
$content .= ' ';
$rank = 1;
foreach ($top_referrers as $referrer) {
$content .= '';
$content .= sprintf('%d ', $rank++);
$content .= sprintf('%s ',
esc_html($referrer->user_name));
$content .= sprintf('%d ',
$referrer->referral_count);
$content .= sprintf('%d ',
$referrer->treated_count);
$content .= ' ';
}
$content .= '
';
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
Referral Statistics
Recent Referrals
Name
Email
Status
Referred At
Actions
referee_name); ?>
referee_email); ?>
status)); ?>
referred_at); ?>
status === 'pending'): ?>
Mark as Treated
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 = '';
if (!$user_id) {
$content .= $this->getUnloggedInReferral();
} else {
$content .= $this->getLoggedInReferral($user_id);
}
$content .= ' ';
$actions[] =[
'button' => '
'.jvbIcon('hand-heart').'
',
'content' => $content
];
return $actions;
}
function getUnloggedInReferral(): string
{
ob_start();
JVB()->connect('cloudflare')->renderTurnstile();
$turnstile = ob_get_clean();
$meta = new MetaForm();
$codeForm = '
Check Your Email!
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.
';
$loginForm = '
Check Your Email!
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.
';
$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();
?>
Your Referral Link
= esc_url($share_url)?>
Copy Link
Your Code
=esc_html($referral_code)?>
Copy 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
= $this->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
= esc_html($total_referrals) ?>
Pending
= esc_html($pending_referrals) ?>
Treated
= esc_html($treated_referrals) ?>
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 : '';
}
}