Jake Vanderwerf
2025-10-20 e729f920139f0c65902be2d6b2c32466b08375e8
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;
   }
}