| | |
| | | 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' |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->cache = Cache::for('referrals', WEEK_IN_SECONDS); |
| | |
| | | |
| | | 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']); |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | 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 |
| | | * |
| | |
| | | * @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; |
| | | } |
| | | |
| | |
| | | * 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 |
| | |
| | | * 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'); |
| | |
| | | $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; |
| | | } |
| | | |
| | |
| | | <p><small>(No data is stored. Your friends will get an email from our email.)</small></p> |
| | | <?php |
| | | $invite = [ |
| | | 'type' => 'tag_list', |
| | | 'type' => 'taglist', |
| | | 'label' => 'Invite Your Friends', |
| | | 'hint' => 'Add friends to send them a referral link', |
| | | 'add_label' => 'Add Invite', |
| | | 'tag_format' => '{name} ({email})', // or 'first_field', 'all_fields', 'email', etc. |
| | | 'tag_format' => '{{name}} ({{email}})', // or 'first_field', 'all_fields', 'email', etc. |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |