From e729f920139f0c65902be2d6b2c32466b08375e8 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 20 Oct 2025 17:54:52 +0000
Subject: [PATCH] =Form updates

---
 inc/managers/ReferralManager.php |  805 +++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 610 insertions(+), 195 deletions(-)

diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 90671f2..9777fa4 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -1,6 +1,10 @@
 <?php
 namespace JVBase\managers;
 
+use JVBase\managers\MagicLinkManager;
+use JVBase\integrations\Cloudflare;
+use JVBase\meta\MetaForm;
+use JVBase\utility\Features;
 use WP_User;
 use WP_Error;
 
@@ -17,6 +21,7 @@
 class ReferralManager
 {
 	protected $wpdb;
+	protected MagicLinkManager $magic_link;
 	protected CacheManager $cache;
 	protected string $referrals_table;
 	protected string $rewards_table;
@@ -38,6 +43,7 @@
 		$this->cache = new CacheManager('referrals');
 		$this->referrals_table = BASE . 'referrals';
 		$this->rewards_table = BASE . 'referral_rewards';
+		$this->magic_link = new MagicLinkManager();
 
 		// Hook into user registration to track referrals
 		add_action('user_register', [$this, 'processReferral'], 10, 1);
@@ -52,15 +58,35 @@
 
 		add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2);
 
-		if (is_user_logged_in()) {
-			add_action('wp_footer', [$this, 'outputShareWidget']);
-		}
 
-		add_action('template_redirect', [$this, 'trackReferralCode']);;
+		add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']);
+
+		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
 		// Schedule cron jobs for reports
 		$this->registerCronJobs();
 	}
 
+	public function enqueueScripts():void
+	{
+		$requirements = [
+			'jvb-utility',
+			'jvb-a11y',
+			'jvb-popup',
+			'jvb-tabs',
+		];
+
+		if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) {
+			$requirements[] = 'cloudflare-turnstile';
+		}
+		wp_enqueue_script(
+			'jvb-referral',
+			JVB_URL . 'assets/js/min/referral.min.js',
+			$requirements,
+			'1.0.0',
+			true
+		);
+	}
+
 	/**
 	 * Register cron jobs for automated reporting
 	 */
@@ -171,8 +197,8 @@
 	 */
 	public function processReferral(int $user_id): void
 	{
-		// Check if there's a referral code in the session/cookie
-		$referral_code = $this->getReferralCodeFromSession();
+		// 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;
@@ -182,20 +208,27 @@
 		$referrer = $this->getUserByReferralCode($referral_code);
 
 		if (!$referrer) {
+			delete_user_meta($user_id, BASE . 'pending_referral_code');
 			return;
 		}
 
-		// Check if this user was already referred (prevent duplicates)
+		// Check for duplicates
 		$existing = $this->getReferralByReferee($user_id);
 		if ($existing) {
+			delete_user_meta($user_id, BASE . 'pending_referral_code');
 			return;
 		}
 
 		// Create referral record
-		$this->createReferral($referrer->ID, $user_id, $referral_code);
+		$result = $this->createReferral($referrer->ID, $user_id, $referral_code);
 
-		// Clear the session
-		$this->clearReferralSession();
+		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);
+		}
 	}
 
 	/**
@@ -232,7 +265,7 @@
 	 * @param string $code
 	 * @return WP_User|null
 	 */
-	protected function getUserByReferralCode(string $code): ?WP_User
+	public function getUserByReferralCode(string $code): ?WP_User
 	{
 		$users = get_users([
 			'meta_key' => BASE . 'referral_code',
@@ -465,40 +498,62 @@
 	 */
 	public function sendDailyReport(): void
 	{
-		// Get referrals from the last 24 hours
-		$referrals = $this->wpdb->get_results(
-			"SELECT r.*, u.display_name as referrer_name, u.user_email as referrer_email
-             FROM {$this->referrals_table} r
-             LEFT JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
-             WHERE r.referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)
-             ORDER BY r.referred_at DESC"
-		);
+		$yesterday = date('Y-m-d', strtotime('-1 day'));
 
-		if (empty($referrals)) {
-			return;  // No referrals, no email
+		// 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;
 		}
 
-		// Generate CSV
-		$csv_content = $this->generateCSV($referrals);
-		$csv_filename = 'referrals-' . date('Y-m-d') . '.csv';
+		// Build email content
+		$content = '<h2>Daily Referral Report</h2>';
+		$content .= '<p><strong>' . count($new_referrals) . '</strong> new referral' .
+			(count($new_referrals) !== 1 ? 's' : '') . ' yesterday (' . $yesterday . ')</p>';
 
-		// Save CSV temporarily
-		$upload_dir = wp_upload_dir();
-		$csv_path = $upload_dir['basedir'] . '/' . $csv_filename;
-		file_put_contents($csv_path, $csv_content);
+		$content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
+		$content .= '<thead><tr style="background: #f5f5f5;">';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referee</th>';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Email</th>';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referrer</th>';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Code</th>';
+		$content .= '</tr></thead><tbody>';
 
-		// Send email with attachment
+		foreach ($new_referrals as $ref) {
+			$content .= '<tr>';
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referee_name));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referee_email));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referrer_name));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referral_code));
+			$content .= '</tr>';
+		}
+
+		$content .= '</tbody></table>';
+
+		// Get admin email
 		$to = get_option('admin_email');
-		$subject = '[' . get_bloginfo('name') . '] Daily Referral Report - ' . date('F j, Y');
+		$subject = sprintf('[%s] %d New Referral%s',
+			get_bloginfo('name'),
+			count($new_referrals),
+			count($new_referrals) !== 1 ? 's' : '');
 
-		$message = $this->generateReportEmail($referrals, 'daily');
 
-		$attachments = [$csv_path];
-
-		wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8'], $attachments);
-
-		// Clean up temporary file
-		unlink($csv_path);
+		jvbMail($to, $subject, $content);
 	}
 
 	/**
@@ -644,35 +699,13 @@
 	 *
 	 * @return array
 	 */
-	protected function getRewardSettings(): array
+	public function getRewardSettings(): array
 	{
 		$saved = get_option(BASE . 'referral_settings', []);
 		return wp_parse_args($saved, $this->default_settings);
 	}
 
 	/**
-	 * Session/Cookie handling for referral codes
-	 */
-	protected function getReferralCodeFromSession(): ?string
-	{
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
-		}
-
-		return $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? null;
-	}
-
-	protected function clearReferralSession(): void
-	{
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
-		}
-
-		unset($_SESSION[BASE . 'referral_code']);
-		setcookie(BASE . 'referral_code', '', time() - 3600, '/');
-	}
-
-	/**
 	 * Display referral info in user profile
 	 *
 	 * @param WP_User $user
@@ -834,28 +867,6 @@
 		update_user_meta($user_id, BASE . 'referral_code', strtoupper($code));
 	}
 
-	public function trackReferralCode(): void
-	{
-		if (!isset($_GET['ref'])) {
-			return;
-		}
-
-		$referral_code = strtoupper(sanitize_text_field($_GET['ref']));
-
-		// Start session if not already started
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
-		}
-
-		// Store in both session and cookie (30 day expiry)
-		$_SESSION[BASE . 'referral_code'] = $referral_code;
-		setcookie(BASE . 'referral_code', $referral_code, time() + (30 * DAY_IN_SECONDS), '/');
-
-		// Optional: Redirect to clean URL (removes ?ref= from address bar)
-		$clean_url = remove_query_arg('ref');
-		wp_safe_redirect($clean_url);
-		exit;
-	}
 
 	/**
 	 * Display user's referral code and share options
@@ -863,141 +874,234 @@
 	 *
 	 * @return string HTML output
 	 */
-	public function outputShareWidget(): string
+	public function outputShareWidget(array $actions):array
 	{
-		$user_id = get_current_user_id();
 
+		$user_id = get_current_user_id();
+		$content = '<aside class="jvb-referral right">';
 		if (!$user_id) {
+			$content .= $this->getUnloggedInReferral();
+		} else {
+			$content .= $this->getLoggedInReferral($user_id);
+		}
+		$content .= '</aside>';
+
+		$actions[] =[
+			'button' => '<button type="button" class="toggle-referral row" title="Your Referrals" data-action="toggle-referral" aria-label="Open Referral Sidebar" aria-controls="referral" aria-expanded="false">
+					'.jvbIcon('hand-heart').'<span class="screen-reader-text"></span>
+				</button>',
+			'content'	=> $content
+		];
+
+		return $actions;
+	}
+
+	function getUnloggedInReferral(): string
+	{
+		ob_start();
+		JVB()->connect('cloudflare')->renderTurnstile();
+		$turnstile = ob_get_clean();
+		$meta = new MetaForm();
+		$codeForm = '<form id="referral-code-form">
+					'.jvbFormStatus().$meta->return('referral_name', null, [
+						'required'	=> true,
+						'type'		=> 'text',
+						'label'		=> 'Your Name',
+						'placeholder'=> 'Mister Meeseeks',
+						'autocomplete'=>'name'
+					]).
+					$meta->return('referral_email', null, [
+						'required'	=> true,
+						'type'		=> 'email',
+						'label'		=> 'Your Email',
+						'placeholder'=> 'look@me.com',
+						'autocomplete'=> 'email'
+					]).
+					$meta->return('referral_code', null, [
+						'required'	=> true,
+						'type'		=> 'text',
+						'label'		=> 'Referral Code',
+						'pattern'	=> '[A-Za-z0-9]+',
+						'maxLength'	=> 20,
+						'autocomplete'=>'off'
+					]).'
+					<button type="submit">
+						Get Started
+					</button>
+
+					<p class="helper-text">
+						We\'ll send you a link to complete your registration.
+					</p>
+					'.$turnstile.'
+				</form><div class="success-content" hidden>
+					<h3>Check Your Email!</h3>
+					<p>We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!</p>
+					<p class="hint">Can\'t find it? Check your spam folder.</p>
+				</div>';
+
+		$loginForm = '<form id ="login-form">
+		'.jvbFormStatus().$meta->return('login_email', null, [
+				'required'	=> true,
+				'type'		=> 'email',
+				'label'		=> 'Your Email',
+				'autocomplete'=>'email'
+			]).'
+		'.$turnstile.'
+		<button type="submit">Login With Magic Link</button>
+</form>
+	<div class="success-content" hidden>
+		<h3>Check Your Email!</h3>
+		<p>We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.</p>
+		<p class="hint">Can\'t find it? Check your spam folder.</p>
+	</div>';
+
+		$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();
+		?>
+		<div class="success-icon">
+			✓
+		</div>
+
+		<div class="success-content">
+			<h3>Success! Your Reward is Ready!</h3>
+
+			<div class="reward-highlight">
+				<p style="margin: 0 0 8px 0;">You'll receive:</p>
+				<p style="margin: 0;"><strong><?php echo esc_html($reward_text); ?> your first treatment!</strong></p>
+			</div>
+
+			<p>Your referral code <strong><?php echo esc_html($code); ?></strong> has been applied. Book your free consultation now to claim your reward!</p>
+
+			<a href="<?php echo esc_url($booking_url); ?>" class="cta-button">
+				Book Your Free Consultation
+			</a>
+
+			<div class="referred-by">
+				Referred by <strong><?php echo esc_html($referrer->display_name); ?></strong>
+			</div>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+	function getLoggedInReferral(int $user_id):string
+	{
+		// Logged-in user widget
 		$referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
 
 		// Generate code if user doesn't have one
 		if (empty($referral_code)) {
-			$manager = new \JVBase\managers\ReferralManager();
-			$referral_code = $manager->getUserReferralCode($user_id);
+			$referral_code = $this->getUserReferralCode($user_id);
+			if (is_wp_error($referral_code)) {
+				return '';
+			}
 		}
 
 		$share_url = home_url('/?ref=' . $referral_code);
-		$encoded_url = urlencode($share_url);
-		$site_name = get_bloginfo('name');
 
 		ob_start();
 		?>
-		<div class="jvb-referral-widget" style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
-			<h3 style="margin-top: 0;">Share & Earn Rewards</h3>
-			<p>Share your unique referral code with friends and earn rewards when they book!</p>
+			<header>
+				<h3>Share the ♡</h3>
+				<p>Invite your friends.</p>
+				<p>Earn rewards when they book!</p>
+			</header>
 
-			<div class="referral-code-display" style="background: white; padding: 15px; border-radius: 4px; margin: 15px 0; text-align: center;">
-				<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Your Referral Code</label>
-				<div style="font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #2271b1;">
-					<?php echo esc_html($referral_code); ?>
-				</div>
-			</div>
-
-			<div class="referral-url" style="margin: 15px 0;">
-				<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Share Link</label>
-				<div style="display: flex; gap: 10px;">
-					<input type="text"
-						   readonly
-						   value="<?php echo esc_url($share_url); ?>"
-						   id="referral-url-<?php echo $user_id; ?>"
-						   style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 14px;">
-					<button type="button"
-							onclick="jvbCopyReferralUrl('referral-url-<?php echo $user_id; ?>')"
-							style="padding: 8px 16px; background: #2271b1; color: white; border: none; border-radius: 4px; cursor: pointer;">
-						Copy
-					</button>
-				</div>
-			</div>
-
-			<div class="referral-share-buttons" style="display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap;">
-				<a href="mailto:?subject=Check out <?php echo esc_attr($site_name); ?>&body=I thought you might like <?php echo esc_url($share_url); ?>"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #666; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					📧 Email
-				</a>
-				<a href="sms:?&body=Check out <?php echo esc_attr($site_name); ?>: <?php echo esc_url($share_url); ?>"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #25D366; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					💬 Text
-				</a>
-				<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $encoded_url; ?>"
-				   target="_blank"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #1877f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					f Facebook
-				</a>
-				<a href="https://twitter.com/intent/tweet?url=<?php echo $encoded_url; ?>&text=Check out <?php echo esc_attr($site_name); ?>"
-				   target="_blank"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #1da1f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					𝕏 Twitter
-				</a>
-			</div>
-
-			<div id="referral-stats-<?php echo $user_id; ?>" class="referral-stats" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
-				<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; text-align: center;">
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="total">-</div>
-						<div style="font-size: 12px; color: #666;">Total Referrals</div>
-					</div>
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #00a32a;" data-stat="treated">-</div>
-						<div style="font-size: 12px; color: #666;">Completed</div>
-					</div>
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #dba617;" data-stat="pending">-</div>
-						<div style="font-size: 12px; color: #666;">Pending</div>
-					</div>
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="rewards">$0</div>
-						<div style="font-size: 12px; color: #666;">Earned</div>
-					</div>
-				</div>
-			</div>
+		<div class="row even share-buttons">
+			<a href="mailto:?subject=<?php echo urlencode('Check out ' . get_bloginfo('name')); ?>&body=<?php echo urlencode('I thought you might be interested: ' . $share_url); ?>"
+			   class="share-btn email-share">
+				<?php echo jvbIcon('envelope', ['size' => 20]); ?>
+				Email
+			</a>
+			<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>"
+			   target="_blank"
+			   rel="noopener noreferrer"
+			   class="share-btn facebook-share">
+				<?php echo jvbIcon('facebook', ['size' => 20]); ?>
+				Facebook
+			</a>
+			<a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode('Check this out!'); ?>"
+			   target="_blank"
+			   rel="noopener noreferrer"
+			   class="share-btn twitter-share">
+				<?php echo jvbIcon('twitter', ['size' => 20]); ?>
+				Twitter
+			</a>
 		</div>
 
-		<script>
-			function jvbCopyReferralUrl(elementId) {
-				const input = document.getElementById(elementId);
-				input.select();
-				document.execCommand('copy');
+			<h4>Your Referral Link</h4>
+			<div class="row btw">
+				<code id="your-referral-link"><?= esc_url($share_url)?></code>
+				<button type="button" class="copy" data-target="your-referral-link">
+					Copy Link
+				</button>
+			</div>
 
-				// Visual feedback
-				const button = input.nextElementSibling;
-				const originalText = button.textContent;
-				button.textContent = 'Copied!';
-				button.style.background = '#00a32a';
 
-				setTimeout(() => {
-					button.textContent = originalText;
-					button.style.background = '#2271b1';
-				}, 2000);
-			}
+			<h4>Your Code</h4>
+			<div class="row btw">
+				<code id="your-referral-code"><?=esc_html($referral_code)?></code>
+				<button type="button" class="copy" data-target="your-referral-code">
+					Copy Code
+				</button>
+			</div>
 
-			// Load stats via AJAX
-			(function() {
-				fetch('<?php echo rest_url(BASE . '/v1/referrals/stats'); ?>', {
-					headers: {
-						'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
-					}
-				})
-					.then(response => response.json())
-					.then(data => {
-						if (data.success && data.stats) {
-							const container = document.getElementById('referral-stats-<?php echo $user_id; ?>');
-							container.querySelector('[data-stat="total"]').textContent = data.stats.total_referrals || 0;
-							container.querySelector('[data-stat="treated"]').textContent = data.stats.treated_count || 0;
-							container.querySelector('[data-stat="pending"]').textContent = data.stats.pending_count || 0;
-							container.querySelector('[data-stat="rewards"]').textContent =
-								'$' + parseFloat(data.stats.available_rewards || 0).toFixed(2);
-						}
-					})
-					.catch(error => console.error('Error loading referral stats:', error));
-			})();
-		</script>
+			<div class="row btw referral-stats">
+				<div class="stat-item">
+					<span class="stat-value" data-stat="total">-</span>
+					<span class="stat-label">Total Referrals</span>
+				</div>
+				<div class="stat-item">
+					<span class="stat-value" data-stat="treated">-</span>
+					<span class="stat-label">Successful</span>
+				</div>
+				<div class="stat-item">
+					<span class="stat-value" data-stat="pending">-</span>
+					<span class="stat-label">Pending</span>
+				</div>
+				<div class="stat-item">
+					<span class="stat-value" data-stat="rewards">$0.00</span>
+					<span class="stat-label">Available Rewards</span>
+				</div>
+			</div>
+
 		<?php
 		return ob_get_clean();
 	}
@@ -1032,5 +1136,316 @@
 
 		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;
+	}
 }
 

--
Gitblit v1.10.0