Jake Vanderwerf
2026-04-15 c4aa5cdb5e90ad4b420e22772797d16980232a2b
inc/managers/ReferralManager.php
@@ -31,6 +31,12 @@
   protected ?int $referralPage = null;
   protected string $rewards_table;
   protected CustomTable $referrals;
   protected CustomTable $codes;
   protected CustomTable $janeClients;
   protected CustomTable $rewards;
   protected CustomTable $treatments;
   // Default reward settings
   protected array $default_settings = [
      'referrer_reward_applies_to' => 'per_user',  // 'per_user' or 'flat_total'
@@ -48,6 +54,7 @@
   public function __construct()
   {
      $this->defineTables();
      global $wpdb;
      $this->wpdb = $wpdb;
      $this->cache = Cache::for('referrals', WEEK_IN_SECONDS);
@@ -63,7 +70,7 @@
      add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3);
      add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1);
      add_action('jvbUserRegistered', [$this, 'processReferral'], 10, 1);
      add_action('user_register', [$this, 'processReferral'], 10, 1);
      // Add meta boxes for admin to manage referrals
      add_action('show_user_profile', [$this, 'displayUserReferralInfo']);
@@ -97,6 +104,182 @@
      add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3);
   }
   protected function defineTables():void
   {
      $this->defineReferralsTable();
      $this->defineCodeTable();
      $this->defineJaneClientsTable();
      $this->defineRewardsTable();
      $this->defineTreatmentsTable();
   }
      protected function defineReferralsTable():void
      {
         $table = CustomTable::for('referrals');
         $table->setColumns([
            'id'     => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
            'from_user' => "{$table->getUserIDType()} NOT NULL",
            'to_user'   => "{$table->getUserIDType()} NOT NULL",
            'to_name'   => 'varchar(255) NOT NULL',
            'to_email'  => 'varchar(255) NOT NULL',
            'to_phone'  => 'varchar(50) NOT NULL',
            'referral_code'=> 'varchar(50) NOT NULL',
            'status' => "ENUM('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'",
            'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP',
            'consulted_at'=> 'datetime DEFAULT NULL',
            'treated_at'=> 'datetime DEFAULT NULL',
            'treatment_count' => 'int DEFAULT 0',
            'notes'     => 'text DEFAULT NULL',
         ]);
         $table->setKeys([
            ['key' => 'PRIMARY', 'value' => 'id'],
            ['key' => 'UNIQUE', 'value' => 'to_user (`to_user`)'],
            'from_user (`from_user`)',
            'status (`status`)',
            'code (`referral_code`)',
            'date (`created_at`)',
            'consult (`consulted_at`)'
         ]);
         $base = BASE;
         $table->setConstraints([
            "CONSTRAINT `{$base}referral_from_user_fk` FOREIGN KEY (`from_user`)
            REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
            "CONSTRAINT `{$base}referral_to_user_fk` FOREIGN KEY (`to_user`)
            REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
         ]);
         $table->defineTable();
         $this->referrals = $table;
      }
      protected function defineCodeTable():void
      {
         $table = CustomTable::for('referrals_codes');
         $table->setColumns([
            'id'     => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
            'user_id'   => "{$table->getUserIDType()} NOT NULL",
            'code'      => 'varchar(50) NOT NULL',
            'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP',
         ]);
         $table->setKeys([
            ['key' => 'PRIMARY', 'value' => 'id'],
            ['key' => 'UNIQUE', 'value' => 'code (`code`)'],
            'user (`user_id`)',
         ]);
         $base = BASE;
         $table->setConstraints([
            "CONSTRAINT `{$base}referral_code_user_fk` FOREIGN KEY (`user_id`)
               REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
         ]);
         $table->defineTable();
         $this->codes = $table;
      }
      protected function defineJaneClientsTable():void
      {
         $table = CustomTable::for('referrals_jane_clients');
         $table->setColumns([
            'id'        => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
            'patient_guid' => 'varchar(50) NOT NULL',
            'user_id'      => "{$table->getUserIDType()} NOT NULL",
            'first_name'   => 'varchar(100) NOT NULL',
            'last_name'    => 'varchar(100) NOT NULL',
            'email'        => 'varchar(255) NOT NULL',
            'imported_at'  => 'datetime DEFAULT CURRENT_TIMESTAMP',
            'updated_at'   => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
         ]);
         $table->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            ['key' => 'UNIQUE', 'value' => 'patient_guid (`patient_guid`)'],
            'user (`user_id`)',
            'email (`email`)',
         ]);
         $base = BASE;
         $table->setConstraints([
            "CONSTRAINT `{$base}jane_clients_user` FOREIGN KEY (`user_id`)
            REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
         ]);
         $table->defineTable();
         $this->janeClients = $table;
      }
      protected function defineRewardsTable():void
      {
         $table = CustomTable::for('referrals_rewards');
         $table->setColumns([
            'id'           => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
            'referral_id'     => 'bigint(20) unsigned NOT NULL',
            'user_id'         => "{$table->getUserIDType()} NOT NULL",
            'reward_type'     => "ENUM('referrer', 'referee') NOT NULL",
            'amount'       => 'decimal(10,2) NOT NULL',
            'reward_calculation'=> "ENUM('percentage', 'fixed')",
            'status'       => "ENUM('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available'",
            'created_at'      => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
            'updated_at'      => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
            'redeemed_at'     => 'datetime DEFAULT NULL',
            'expires_at'      => 'datetime DEFAULT NULL',
            'notes'           => 'text DEFAULT NULL',
         ]);
         $table->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            'referral (`referral_id`)',
            'user (`user_id`)',
            'status (`status`)',
            'type (`reward_type`)'
         ]);
         $base = BASE;
         $table->setConstraints([
            "CONSTRAINT `{$base}reward_referral` FOREIGN KEY (`referral_id`)
            REFERENCES {$this->referrals->getFullTableName()} (`id`) ON DELETE CASCADE",
            "CONSTRAINT `{$base}reward_user` FOREIGN KEY (`user_id`)
            REFERENCES {$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
         ]);
         $table->defineTable();
         $this->rewards = $table;
      }
      protected function defineTreatmentsTable():void
      {
         $table = CustomTable::for('referral_treatments');
         $table->setColumns([
            'id'        => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
            'referral_id'  => 'bigint(20) unsigned NOT NULL',
            'user_id'      => "{$table->getUserIDType()} NOT NULL",
            'treatment_type'=> 'varchar(100) NOT NULL',     //Tier 1-6, Brows, etc
            'treatment_date'=> 'datetime NOT NULL',
            'invoice_number'=> 'varchar(50) DEFAULT NULL',
            'amount'    => 'decimal(10,2) DEFAULT NULL',
            'status'    => "ENUM('completed', 'no_show', 'cancelled') DEFAULT 'completed'",
            'imported_at'  => 'datetime DEFAULT CURRENT_TIMESTAMP',
         ]);
         $table->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            'referral (`referral_id`)',
            'user (`user_id`)',
            'date (`treatment_date`)',
            'type (`treatment_type`)',
         ]);
         $base = BASE;
         $table->setConstraints([
            "CONSTRAINT `{$base}treatment_referral` FOREIGN KEY (`referral_id`)
            REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE",
            "CONSTRAINT `{$base}treatment_user` FOREIGN KEY (`user_id`)
            REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
         ]);
         $table->defineTable();
         $this->treatments = $table;
      }
   public function getSettings():array
   {
      return $this->settings;
@@ -186,6 +369,26 @@
      }
   }
   public function createCode(int $user_id, string $code):string|false
   {
      $code = sanitize_title($code);
      $existing = $this->codes->get(['code' => $code]);
      if ($existing) {
         if ($existing['user_id'] !== $user_id) {
            return false;
         }
         return $code;
      }
      $success = $this->codes->insert([
         'user_id'   => $user_id,
         'code'      => $code
      ]);
      if ($success) {
         return $code;
      }
      return false;
   }
   /**
    * Generate or get existing referral code for a user
    *
@@ -193,31 +396,34 @@
    * @param string|null $custom_code Optional custom code
    * @return string|WP_Error
    */
   public function getUserReferralCode(int $user_id, ?string $custom_code = null)
   public function getUserReferralCode(int $user_id, ?string $custom_code = null):array|wp_error
   {
      $user = get_userdata($user_id);
      if (!$user) {
         return new WP_Error('invalid_user', 'User not found');
      }
      // Check if user already has a code
      $existing_code = get_user_meta($user_id, BASE . 'referral_code', true);
      if ($existing_code && !$custom_code) {
         return $existing_code;
      $existing = $this->codes->pluck('code', ['user_id' => $user_id],'created_at', 'DESC');
      if ($existing && !$custom_code) {
         return $existing;
      }
      if ($custom_code && !in_array($custom_code, $existing)) {
         $test = $this->createCode($user_id, $custom_code);
         if ($test) {
            return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC');
         }
      } else {
         return $existing;
      }
      // Generate new code if custom provided or none exists
      $code = $custom_code ?: $this->generateReferralCode($user);
      $code = $this->generateReferralCode($user);
      // Validate uniqueness
      if ($this->isCodeTaken($code, $user_id)) {
         return new WP_Error('code_taken', 'This referral code is already in use');
      $success = $this->createCode($user_id, $code);
      if ($success) {
         return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC');
      }
      // Save the code
      update_user_meta($user_id, BASE . 'referral_code', $code);
      return $code;
   }
@@ -251,24 +457,11 @@
    * Check if a referral code is already taken
    *
    * @param string $code
    * @param int|null $exclude_user_id
    * @return bool
    */
   protected function isCodeTaken(string $code, ?int $exclude_user_id = null): bool
   protected function isCodeTaken(string $code): bool
   {
      $args = [
         'meta_key' => BASE . 'referral_code',
         'meta_value' => $code,
         'fields' => 'ID',
         'number' => 1
      ];
      if ($exclude_user_id) {
         $args['exclude'] = [$exclude_user_id];
      }
      $users = get_users($args);
      return !empty($users);
      return (bool) $this->codes->get(['code' => $code]);
   }
   public function processRegistrationToken(int $user_id, string $email, array $data): void
@@ -295,60 +488,58 @@
    * Track a new referral when user registers
    *
    * @param int $user_id
    * @param array $userData
    * @return bool;
    */
   public function processReferral(int $user_id): bool
   public function processReferral(int $user_id, array $userData): bool
   {
      // Try to get code from user meta first (set during registration)
      $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true);
      $referral = $this->referrals->get(['to_user' => $user_id]);
      if (empty($referral_code)) {
      if (empty($referral)) {
         $referral = $this->referrals->get(['to_email' => $userData['email']]);
      }
      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']
            ];
         }
      }
      if (empty($referral_code)) {
      if (empty($referral)) {
         return false; // No referral code - regular registration
      }
      // Find the referrer
      $referrer = $this->getUserByReferralCode($referral_code);
      if (!$referrer) {
         delete_user_meta($user_id, BASE . 'pending_referral_code');
      $referrer = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]);
      if (empty($referrer)) {
         //This should not happen, but whatever
         return false;
      }
      $referrer = $referrer[0];
      $record = $this->referrals->findOrCreate([
         'to_user'   => $user_id,
         'referral_code'   => $referral['referral_code'],
      ], [
         'from_user'    => $referrer,
         '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;
      }
      $user = get_userdata($user_id);
      // Check if referral already exists for this user
      $existing = $this->wpdb->get_row($this->wpdb->prepare(
         "SELECT * FROM {$this->referrals_table}
      WHERE referrer_id = %d AND (referee_email = %s OR referee_id = %d)",
         $referrer->ID,
         $user->user_email,
         $user_id
      ));
      if (!$existing) {
         // Create new referral record - referred_at captures registration time
         $this->wpdb->insert(
            $this->referrals_table,
            [
               'referrer_id' => $referrer->ID,
               'referee_id' => $user_id,
               'referee_name' => $user->display_name,
               'referee_email' => $user->user_email,
               'referee_phone' => get_user_meta($user_id, BASE . 'phone', true) ?: '',
               'referral_code' => $referral_code,
               'status' => 'pending', // pending first treatment
               'referred_at' => current_time('mysql') // When they registered
            ],
            ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
         );
      }
      // Clean up temp data
      delete_user_meta($user_id, BASE . 'pending_referral_code');
@@ -363,10 +554,10 @@
      $this->cache->flush();
      // Fire action for tracking
      do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code);
      do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']);
      // Send notification to referrer
      $this->sendReferrerNotification($referrer->ID, $user->display_name);
      $this->sendReferrerNotification($referrer->ID, $userData['display_name']);
      return true;
   }