From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
inc/managers/Referrals.php | 791 ++++++++++++++++++++++++++++---------------------------
1 files changed, 402 insertions(+), 389 deletions(-)
diff --git a/inc/managers/ReferralManager.php b/inc/managers/Referrals.php
similarity index 85%
rename from inc/managers/ReferralManager.php
rename to inc/managers/Referrals.php
index 626d036..71769f7 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/Referrals.php
@@ -1,7 +1,13 @@
<?php
namespace JVBase\managers;
+use JVBase\Instance;
+use JVBase\integrations\services\Cloudflare;
use JVBase\meta\Form;
+use JVBase\meta\Meta;
+use JVBase\meta\Sanitizer;
+use JVBase\rest\Rest;
+use JVBase\rest\routes\ReferralRoutes;
use JVBase\ui\CRUDSkeleton;
use JVBase\ui\Tabs;
use JVBase\base\Site;
@@ -18,9 +24,10 @@
* Handles user referral codes, tracking, rewards, and reporting.
* Keeps referrals separate from invitations system.
*/
-class ReferralManager
+class Referrals
{
- protected MagicLinkManager $magic_link;
+ use Instance;
+ protected MagicLinks $magic_link;
protected Cache $cache;
protected Cache $requestCache;
protected Cache $statsCache;
@@ -33,6 +40,7 @@
protected CustomTable $janeClients;
protected CustomTable $rewards;
protected CustomTable $treatments;
+ protected Rest $route;
// Default reward settings
protected array $default_settings = [
@@ -48,10 +56,11 @@
protected array $settings;
- public function __construct()
+ private function __construct()
{
+ $this->route = ReferralRoutes::getInstance();
$this->defineTables();
- $this->role = Site::getDefaultReferralRole();
+ $this->role = Site::getReferralRole();
$this->default_settings['referral_role'] = $this->role;
$this->cache = Cache::for('referrals', WEEK_IN_SECONDS);
@@ -88,9 +97,6 @@
// Schedule cron jobs for reports
$this->registerCronJobs();
- // 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']);
@@ -102,6 +108,8 @@
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
@@ -118,14 +126,15 @@
$table->setColumns([
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
- 'from_user' => "{$table->getUserIDType()} NOT NULL",
- 'to_user' => "{$table->getUserIDType()} NOT NULL",
- 'to_name' => 'varchar(255) NOT NULL',
- 'to_email' => 'varchar(255) NOT NULL',
- 'to_phone' => 'varchar(50) NOT NULL',
+ '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('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'",
+ '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',
@@ -134,8 +143,9 @@
$table->setKeys([
['key' => 'PRIMARY', 'value' => 'id'],
- ['key' => 'UNIQUE', 'value' => '(`to_user`)'],
+// ['key' => 'UNIQUE', 'value' => '(`to_user`)'],
'from_user (`from_user`)',
+ 'to_user (`to_user`)',
'status (`status`)',
'code (`referral_code`)',
'date (`created_at`)',
@@ -329,8 +339,8 @@
'jvb-data-store',
];
- if (Site::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) {
- JVB()->connect('cloudflare')->enqueueTurnstileScripts();
+ if (Site::hasIntegration('cloudflare') && Cloudflare::getInstance()->isSetUp()) {
+ Cloudflare::getInstance()->enqueueTurnstileScripts();
}
if (is_singular(BASE.'dash')) {
$requirements[] = 'jvb-form';
@@ -489,109 +499,140 @@
*
* @param int $user_id
* @param array $userData
- * @return bool;
+ * @return void
*/
- public function processReferral(int $user_id, array $userData): bool
+ public function processReferral(int $user_id, array $userData):void
{
- $referral = $this->referrals->get(['to_user' => $user_id]);
- if (empty($referral)) {
- $referral = $this->referrals->get(['to_email' => $userData['email']]);
+ if (!array_key_exists('ref', $_POST) || empty($_POST['ref'])) {
+ return;
}
- if (empty($referral)) {
- // Check session/cookie if not in meta
- if (session_status() === PHP_SESSION_NONE) {
- session_start();
- }
- $referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? '';
- if (!empty ($referral_code)) {
- $referral = [
- 'to_user' => $user_id,
- 'referral_code' => $referral_code,
- 'to_email' => $userData['user_email']
- ];
- }
+ $code = sanitize_text_field($_POST['ref']);
+ $referredBy = $this->codes->pluck('user_id', ['code' => $code]);
+ if (!empty($referredBy)) {
+ $this->createReferralFromID($referredBy[0], $user_id, $code);
}
-
- if (empty($referral)) {
- return false; // No referral code - regular registration
- }
-
- // Find the from_user
- $from_user = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]);
- if (empty($from_user)) {
- //This should not happen, but whatever
- return false;
- }
-
- // Check if this email already has a referral record
- if ($this->isEmailInvited($referral['to_email'])) {
- return false;
- }
-
- $from_user = $from_user[0];
- $record = $this->referrals->findOrCreate([
- 'to_user' => $user_id,
- 'referral_code' => $referral['referral_code'],
- ], [
- 'from_user' => $from_user,
- 'to_email' => $referral['to_email'],
- 'to_name' => $userData['first_name'],
-// 'to_phone' =>
- 'status' => 'pending'
- ]);
-
- if (!$record) {
- error_log('[ReferralManager]::processReferral Could not update record for user: '.print_r($referral, true));
- return false;
- }
-
-
- // Clean up temp data
- delete_user_meta($user_id, BASE . 'pending_referral_code');
- if (isset($_SESSION[BASE . 'referral_code'])) {
- unset($_SESSION[BASE . 'referral_code']);
- }
- if (isset($_COOKIE[BASE . 'referral_code'])) {
- setcookie(BASE . 'referral_code', '', time() - 3600, '/');
- }
-
- // Clear caches
- $this->cache->flush();
-
- // Fire action for tracking
- do_action('jvb_referral_processed', $user_id, $from_user->ID, $referral['referral_code']);
-
- // Send notification to from_user
- $this->sendReferrerNotification($from_user->ID, $userData['display_name']);
- return true;
}
/**
* Create a referral record in the database
*
- * @param int $from_user_id
- * @param int $to_user_id
- * @param string $code
- * @return int|false
+ * @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 createReferral(int $from_user_id, int $to_user_id, string $code)
+ public function createReferralFromFields(int $from_user_id, string $name, string $email, ?string $phone = null, ?string $code = null):array
{
- $user = get_user_by('ID', $to_user_id);
+ if (!get_userdata($from_user_id)) {
+ return [
+ 'success' => false,
+ 'message' => 'From user does not exist',
+ ];
+ }
- return $this->referrals->findOrCreate([
- [
- 'to_user' => $to_user_id,
- ],
- [
- 'from_user' => $from_user_id,
- 'to_name' => $user->display_name,
- 'to_email' => $user->user_email,
- 'to_phone' => get_user_meta($to_user_id, BASE . 'phone', true) ?: '',
- 'referral_code' => $code,
- 'status' => 'pending',
- ]
- ]);
+ 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'
+ ];
}
/**
@@ -714,35 +755,49 @@
'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' => [
- 'from_user' => $user_id,
- ],
+ 'where' => $where,
'per_page' => $args['per_page'],
'page' => $args['page'],
'orderby' => $args['orderby'],
'order' => $args['order']
];
- if (in_array($args['status'], ['pending', 'consulted', 'treated', 'cancelled'])){
+
+ if ($args['status'] !== 'all'){
$tableArgs['status'] = $args['status'];
}
- $referrals = $this->referrals->getMany($tableArgs);
+ $referrals = $this->referrals->getMany($tableArgs, false);
return array_map(function($referral) {
- $last_invite = get_transient('referral_last_invite_' . md5($referral->to_email));
- $can_resend = !$last_invite || (time() - $last_invite) > WEEK_IN_SECONDS;
+ $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' => JVB()->routes('referral')->formatTimestamp($referral->referred_at),
+ 'referred_at' => ReferralRoutes::getInstance()->formatTimestamp($referral->referred_at),
'referral_status'=> $status,
'can_resend' => $can_resend
];
@@ -750,6 +805,17 @@
}
+ 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
*
@@ -761,25 +827,15 @@
return $this->statsCache->remember(
$user_id,
function() use ($user_id) {
- $stats = $this->referrals->queryResults(
- "SELECT
- COUNT(*) as code_used,
- SUM(CASE WHEN status IN ('consulted', 'treated') THEN 1 ELSE 0 END) as consultations,
- SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treatments,
- SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending
- FROM {table}
- WHERE from_user = %d",
- [$user_id],
- ARRAY_A
- );
- $stats = $stats[0] ?? [];
-
- $rewards = $this->rewards->queryVar(
- "SELECT SUM(amount) FROM {table} WHERE user_id = %d AND reward_type = 'from_user'",
- [$user_id]
- );
-
- $stats['total_rewards'] = floatval($rewards ?? 0);
+ $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;
}
@@ -852,15 +908,15 @@
return;
}
- $content = JVB()->email()->h1('Daily Referral Report');
- $content .= JVB()->email()->stat(
+ $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 .= JVB()->email()->spacer(20);
- $content .= JVB()->email()->h2('New Referrals');
+ $content .= Emails::getInstance()->spacer(20);
+ $content .= Emails::getInstance()->h2('New Referrals');
// Build list of referrals
foreach ($new_referrals as $ref) {
@@ -871,12 +927,12 @@
);
$cardContent .= sprintf(
'<p style="font-size:13px;color:%s;">Referred by: %s | Code: %s</p>',
- JVB()->email()->colours['dark-200'],
+ Emails::getInstance()->colours['dark-200'],
esc_html($ref->from_name),
- JVB()->email()->badge($ref->referral_code, 'info')
+ Emails::getInstance()->badge($ref->referral_code, 'info')
);
- $content .= JVB()->email()->card($cardContent);
+ $content .= Emails::getInstance()->card($cardContent);
}
$to = get_option('admin_email');
@@ -887,7 +943,7 @@
count($new_referrals) !== 1 ? 's' : ''
);
- JVB()->email()->sendEmail($to, $subject, $content, 'DAILY REPORT');
+ Emails::getInstance()->sendEmail($to, $subject, $content, 'DAILY REPORT');
}
/**
@@ -895,31 +951,29 @@
*/
public function sendWeeklyReport(): void
{
- $top_from_users = $this->getTopReferrers(10, 'week');
- $total_referrals = $this->referrals->queryVar(
- "SELECT COUNT(*) FROM {table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)"
- );
+ $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 = JVB()->email()->h1('Weekly Referral Summary');
- $content .= JVB()->email()->stat(
+ $content = Emails::getInstance()->h1('Weekly Referral Summary');
+ $content .= Emails::getInstance()->stat(
$total_referrals,
'Total Referrals',
'This week'
);
- $content .= JVB()->email()->spacer(30);
- $content .= JVB()->email()->h2('Top 10 Referrers');
+ $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
- ? JVB()->email()->badge('#' . $rank, $rank === 1 ? 'success' : 'info')
- : '<span style="font-weight:600;color:' . JVB()->email()->colours['dark-200'] . ';">#' . $rank . '</span>';
+ ? Emails::getInstance()->badge('#' . $rank, $rank === 1 ? 'success' : 'info')
+ : '<span style="font-weight:600;color:' . Emails::getInstance()->colours['dark-200'] . ';">#' . $rank . '</span>';
$cardContent = sprintf(
'<p>%s <strong>%s</strong></p>',
@@ -928,19 +982,19 @@
);
$stats = [
- JVB()->email()->stat($from_user->referral_count, 'Total Referrals'),
- JVB()->email()->stat($from_user->treated_count, 'Treated')
+ Emails::getInstance()->stat($from_user->referral_count, 'Total Referrals'),
+ Emails::getInstance()->stat($from_user->treated_count, 'Treated')
];
- $cardContent .= JVB()->email()->grid($stats, 2);
+ $cardContent .= Emails::getInstance()->grid($stats, 2);
- $content .= JVB()->email()->card($cardContent);
+ $content .= Emails::getInstance()->card($cardContent);
$rank++;
}
$to = get_option('admin_email');
$subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y');
- JVB()->email()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY');
+ Emails::getInstance()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY');
}
/**
@@ -1005,7 +1059,7 @@
];
}
- $content .= JVB()->email()->table($from_users, 'Top 10 Referrers This Week');
+ $content .= Emails::getInstance()->table($from_users, 'Top 10 Referrers This Week');
return $content;
}
@@ -1220,7 +1274,7 @@
function getUnloggedInReferral(): string
{
ob_start();
- JVB()->connect('cloudflare')->renderTurnstile();
+ Cloudflare::getInstance()->renderTurnstile();
$turnstile = ob_get_clean();
$reward_text = $this->getRewardText(true);
@@ -1512,8 +1566,8 @@
<p>Or click the button below:</p>
%s
</div>',
- JVB()->email()->link($code),
- JVB()->email()->button($share_url, 'Share Your Code')
+ Emails::getInstance()->link($code),
+ Emails::getInstance()->button($share_url, 'Share Your Code')
);
}
@@ -1555,31 +1609,39 @@
return new WP_Error('invalid_email', 'Invalid email address');
}
+
+
// Check if already registered
if (email_exists($invitee_email)) {
return new WP_Error('user_exists', 'This person already has an account');
}
// Get from_user info
- $from_user = get_user_by('ID', $user_id);
+ $from_user = get_userdata($user_id);
$referral_code = $this->getUserReferralCode($user_id);
- if ($referral_code) {
- return $referral_code;
+ // Record the invitation attempt (for rate limiting only)
+ if ($this->emailAlreadyTreated($invitee_email)) {
+ return [
+ 'success' => false,
+ 'message' => 'User already treated'
+ ];
}
- // Record the invitation attempt (for rate limiting only)
- $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name);
-
- // Create registration URL with token (opens sidebar with prefilled form)
- $token_data = [
- 'name' => sanitize_text_field($invitee_name),
- 'email' => $invitee_email,
- 'expires' => time() + (30 * DAY_IN_SECONDS)
- ];
+ $record = $this->referrals->findOrCreate(
+ [
+ 'from_user' => $user_id,
+ 'to_email' => $invitee_email,
+ 'referral_code' => $referral_code,
+ ],
+ [
+ 'to_name' => $invitee_name,
+ 'status' => 'invited',
+ 'last_invited' => current_time('mysql')
+ ]
+ );
// Encode the token
- $token = base64_encode(json_encode($token_data));
$registration_url = add_query_arg([
'ref' => $referral_code,
'rname' => sanitize_text_field($invitee_name),
@@ -1606,11 +1668,11 @@
esc_html(get_bloginfo('name')),
nl2br(esc_html($message)),
esc_html($reward_text),
- JVB()->email()->button($registration_url, 'Register & Get Your Reward')
+ Emails::getInstance()->button($registration_url, 'Register & Get Your Reward')
);
// Send email
- $sent = JVB()->email()->sendEmail(
+ $sent = Emails::getInstance()->sendEmail(
$invitee_email,
$subject,
$email_content
@@ -1642,26 +1704,27 @@
'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;
+ $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 (is_wp_error($result)) {
+ if (!$result['success']) {
$results['failed'][] = [
'email' => $email,
'name' => $name,
- 'reason' => $result->get_error_message()
+ 'reason' => $result['message']
];
} else {
$results['success'][] = [
@@ -1693,98 +1756,25 @@
*/
protected function checkEmailRateLimit(int $user_id):bool|string
{
- $hourly_key = 'referral_invites_hour_' . $user_id;
- $count = (int) get_transient($hourly_key);
+ $key = BASE.'referral_email_limit';
+ $stored = get_user_meta($user_id, $key, true);
+ $count = empty($stored) ? 0 : count($stored);
if ($count >= 15) {
- return 'hourly_limit_reached';
+ return false;
}
- set_transient($hourly_key, $count + 1, HOUR_IN_SECONDS);
+ $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;
}
/**
- * 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->referrals->pluck('id', ['to_email' => $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
@@ -1871,22 +1861,12 @@
'menu_title' => 'Referrals',
'capability' => 'manage_options',
'menu_slug' => BASE . 'referral-admin',
- 'callback' => [self::class, 'renderAdminPageStatic']
+ 'callback' => [self::class, 'renderAdminPage']
];
AdminPages::addSubPage(BASE.'referral-admin', $subpage);
}
/**
- * Static wrapper for renderAdminPage
- * Called by WordPress when admin page is rendered
- */
- public static function renderAdminPageStatic(): void
- {
- // Get the properly initialized instance from JVB singleton
- JVB()->referrals()->renderAdminPage();
- }
-
- /**
* Register settings
*/
public function registerSettings(): void
@@ -2496,95 +2476,42 @@
*/
protected function renderReferralStats(bool $wrapCard = false): string
{
- global $wpdb; // Use fresh global instead of stored reference
+ $isAdmin = current_user_can('manage_options');
+ $current = get_current_user_id();
+ $types = ['total' => 'Total Referrals', 'invited' => 'Invited', 'pending' => 'Pending', 'treated' => 'Treated'];
- ob_start();
+ $out = '<table class="widefat">';
+ foreach ($types as $type => $label) {
+ $status = $type === 'total' ? [] : ['status' => $type];
- // Get fresh table name references
- $referrals_table = $wpdb->prefix . BASE . 'referrals';
-
- // Use proper WordPress prepare even for COUNT
- $total_referrals = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT COUNT(*) FROM `{$referrals_table}` WHERE 1=%d",
- 1
- )
- );
-
- $pending_referrals = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s",
- 'pending'
- )
- );
-
- $treated_referrals = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s",
- 'treated'
- )
- );
-
- ?>
- <table class="widefat">
- <tr>
- <th>Total Referrals</th>
- <td><?= esc_html($total_referrals ?? 0) ?></td>
- </tr>
- <tr>
- <th>Pending</th>
- <td><?= esc_html($pending_referrals ?? 0) ?></td>
- </tr>
- <tr>
- <th>Treated</th>
- <td><?= esc_html($treated_referrals ?? 0) ?></td>
- </tr>
- </table>
- <?php
- $table = ob_get_clean();
+ $count = $isAdmin ?
+ $this->referrals->count($status) :
+ $this->referrals->count(array_merge(
+ ['from_user' => $current],
+ $status
+ ));
+ $out .= sprintf(
+ '<tr>
+ <th>%s</th>
+ <td>%s</td>
+ </tr>',
+ $label,
+ esc_html($count)
+ );
+ }
+ $out .= '</table>';
if ($wrapCard) {
- $table = '<div class="card">
+ $out = '<div class="card">
<h2>Referral Statistics</h2>
- ' . $table . '
+ ' . $out . '
</div>';
}
- return $table;
+ return $out;
}
- /**
- * 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;
- }
- if (!$this->referralPage) {
- $this->referralPage = $this->getReferralPageId();
- }
- if (!$this->referralPage) {
- return;
- }
-
- global $pagenow, $post;
-
- // Check if we're editing the referral page
- if ('post.php' === $pagenow && $post && $post->ID === $this->referralPage) {
- $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
@@ -2612,7 +2539,7 @@
}
if ($post->ID === $this->referralPage) {
echo '<div class="notice notice-info">';
- echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvbase') . '</p>';
+ echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvb') . '</p>';
echo '</div>';
}
}
@@ -2826,7 +2753,7 @@
* @param array $post_data POST data
* @return array|null Result array or null if not our page
*/
- public function handleAdminSubmission($result, string $page_slug, array $post_data): ?array
+ public function handleAdminSubmission(mixed $result, string $page_slug, array $post_data): ?array
{
// Only handle our page
if ($page_slug !== BASE . 'referral-admin') {
@@ -2961,7 +2888,7 @@
home_url('/dash/referrals')
);
- JVB()->email()->sendEmail(
+ Emails::getInstance()->sendEmail(
$from_user->user_email,
$subject,
$message
@@ -3033,11 +2960,14 @@
return ob_get_clean();
}
- public function updateStatus(int $referral_id, string $status): bool|WP_Error
+ public function updateStatus(int $referral_id, string $status):array
{
$referral = $this->referrals->get(['id' => $referral_id]);
if (!$referral) {
- return new WP_Error('not_found', 'Referral not found');
+ return [
+ 'success' => false,
+ 'message' => 'Referral not found'
+ ];
}
$data = ['status' => $status, "{$status}_at" => current_time('mysql')];
@@ -3046,48 +2976,78 @@
}
$result = $this->referrals->update($data, ['id' => $referral_id]);
- if ($result === false) {
- return new WP_Error('update_failed', 'Failed to update referral status');
+ if (!$result) {
+ return [
+ 'success' => false,
+ 'message' => 'Failed to update referral status'
+ ];
}
if ($status === 'treated') {
$this->createRewardRecords($referral_id);
}
- $this->cache->flush();
- return true;
+ $this->cache->forget($referral_id);
+ return [
+ 'success' => true,
+ 'message' => 'Successfully set status to '.$status
+ ];
}
- public function removeReferral(int $referral_id, int $user_id): bool|WP_Error
+ public function removeReferral(int $referral_id, int $user_id):array
{
$referral = $this->referrals->get(['id' => $referral_id]);
if (!$referral) {
- return new WP_Error('not_found', 'Referral not found');
+ return [
+ 'success' => false,
+ 'message' => 'Referral not found'
+ ];
}
if ($referral->from_user != $user_id && !current_user_can('manage_options')) {
- return new WP_Error('unauthorized', 'Unauthorized');
+ return [
+ 'success' => false,
+ 'message' => 'Unauthorized'
+ ];
}
- if ($referral->status !== 'pending') {
- return new WP_Error('invalid_status', 'Can only remove pending referrals');
+ if (!in_array($referral->status, ['invited', 'pending'])) {
+ return [
+ 'success' => false,
+ 'message' => 'Cannot remove a completed or cancelled referral'
+ ];
}
$this->referrals->delete(['id' => $referral_id]);
- $this->cache->flush();
- return true;
+ $this->cache->forget($referral_id);
+ return [
+ 'success' => true,
+ 'message' => 'Successfully removed referral'
+ ];
}
- public function resendInvitation(int $referral_id, int $user_id): bool|WP_Error
+ public function resendInvitation(int $referral_id, int $user_id):array
{
$referral = $this->referrals->where(['id' => $referral_id, 'from_user' => $user_id])->first();
if (!$referral) {
- return new WP_Error('not_found', 'Referral not found');
+ return [
+ 'success' => false,
+ 'message' => 'Referral not found'
+ ];
}
- $transient_key = 'referral_last_invite_' . md5($referral->to_email);
- if (get_transient($transient_key)) {
- return new WP_Error('rate_limit', 'Can only resend once per week');
+ if (!$this->checkCanResendEmail($referral)) {
+ $days = (time() - $referral->last_invited) / DAY_IN_SECONDS;
+ $days = match (true) {
+ $days <= 1 => 'until tomorrow',
+ $days <= 3 => 'a couple days',
+ default => absint($days).' days'
+ };
+
+ return [
+ 'success' => false,
+ 'message' => 'Can\'t resend email. Wait '.$days.'.'
+ ];
}
$result = $this->sendReferralInvitation(
@@ -3098,15 +3058,13 @@
'Just a friendly reminder about my invitation!'
);
- if (is_wp_error($result)) {
- return $result;
- }
-
- set_transient($transient_key, time(), WEEK_IN_SECONDS);
- return true;
+ return [
+ 'success' => (bool) $result,
+ 'message' => $result ? 'Sent email' : 'Could not send email'
+ ];
}
- public function getAllReferrals(array $args = []): array
+ public function getReferrals(array $args = []): array
{
$conditions = ['1=1'];
$values = [];
@@ -3145,5 +3103,60 @@
$values
);
}
+
+ public function emailAlreadyTreated(string $email):bool
+ {
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ return false;
+ }
+ if (email_exists($email)) {
+ return false;
+ }
+ $referral = $this->referrals->get([
+ 'to_email' => $email,
+ 'status' => [
+ 'IN',
+ ['pending', 'consulted', 'treated', 'cancelled']
+ ]
+ ]);
+ return !empty($referral);
+ }
+
+
+ /**
+ * Process queued referral operations
+ */
+ public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error
+ {
+ if ($operation->type !== 'referral_invite') {
+ return $result;
+ }
+
+ $result = $this->sendBatchReferralInvitations(
+ $operation->user_id,
+ $data['invitations'],
+ $data['subject'],
+ $data['message']
+ );
+
+ if ($result['success']) {
+ $this->cache->flush();
+ }
+
+ return [
+ 'success' => true,
+ 'message' => sprintf(
+ 'Sent invitations. Success: %d. Failed: %d.',
+ count($result['result']['success']),
+ count($result['result']['failed'])
+ ),
+ 'details' => [
+ 'successful' => $result['result']['success'],
+ 'failed' => $result['result']['failed'],
+ 'total' => count($data['invitations'])
+ ]
+ ];
+ }
+
}
--
Gitblit v1.10.0