From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve

---
 inc/rest/routes/ReferralRoutes.php |  546 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 538 insertions(+), 8 deletions(-)

diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index 15f293e..b8f198c 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -1,7 +1,8 @@
 <?php
 namespace JVBase\rest\routes;
 
-use JVBase\managers\ReferralManager;
+use JVBase\importers\JaneAppClientImporter;
+use JVBase\managers\JaneSalesImporter;
 use JVBase\rest\RestRouteManager;
 use WP_REST_Request;
 use WP_REST_Response;
@@ -16,11 +17,24 @@
  */
 class ReferralRoutes extends RestRouteManager
 {
-	protected ReferralManager $manager;
+	protected string $referrals_table;
+	protected string $rewards_table;
+	protected string $treatments_table;
+	protected string $jane_clients_table;
+	protected $wpdb;
+
 	public function __construct()
 	{
 		$this->route = 'referrals';
+		$this->cache_name = 'referrals';
 		parent::__construct();
+
+		global $wpdb;
+		$this->wpdb = $wpdb;
+		$this->referrals_table = $wpdb->prefix . BASE . 'referrals';
+		$this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards';
+		$this->treatments_table = $wpdb->prefix . BASE . 'referral_treatments';
+		$this->jane_clients_table = $wpdb->prefix . BASE . 'jane_clients';
 	}
 
 	public function registerRoutes(): void
@@ -235,6 +249,70 @@
 				]
 			]
 		]);
+
+
+		/***************************
+		 * ADDITIONAL
+		 */
+// CSV Uploads
+		register_rest_route($this->namespace, '/referrals/upload-clients', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleClientUpload'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		register_rest_route($this->namespace, '/referrals/upload-sales', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleSalesUpload'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		// Referral List & Details
+		register_rest_route($this->namespace, '/referrals/list', [
+			'methods' => 'GET',
+			'callback' => [$this, 'getReferralsList'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		register_rest_route($this->namespace, '/referrals/(?P<id>\d+)', [
+			'methods' => 'GET',
+			'callback' => [$this, 'getReferralDetails'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		// Manual Status Updates
+		register_rest_route($this->namespace, '/referrals/mark-consulted', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleMarkConsulted'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		register_rest_route($this->namespace, '/referrals/mark-treated', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleMarkTreated'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		// User-facing endpoints
+		register_rest_route($this->namespace, '/referrals/my-stats', [
+			'methods' => 'GET',
+			'callback' => [$this, 'getMyStats'],
+			'permission_callback' => [$this, 'checkPermission']
+		]);
+
+		register_rest_route($this->namespace, '/referrals/my-referrals', [
+			'methods' => 'GET',
+			'callback' => [$this, 'getMyReferrals'],
+			'permission_callback' => [$this, 'checkPermission']
+		]);
+	}
+
+	/**
+	 * Check admin-only permission
+	 */
+	public function checkAdminPermission(WP_REST_Request $request): bool
+	{
+		return current_user_can('manage_options') && parent::checkPermission($request);
 	}
 
 	public function checkPermission(WP_REST_Request $request): bool
@@ -620,27 +698,24 @@
 
 	public function addReferralCodePostRegistration(WP_REST_Request $request): WP_REST_Response
 	{
-		if (!$this->manager) {
-			$this->manager = JVB()->referrals();
-		}
 		$user_id = get_current_user_id();
 		$code = $request->get_param('code');
 
 		// Check if user already has a referral (can't change)
-		$existing = $this->manager->getReferralByReferee($user_id);
+		$existing = JVB()->referrals()->getReferralByReferee($user_id);
 		if ($existing) {
 			return $this->error('You already have a referral code applied', 'already_referred', 400);
 		}
 
 		// Validate the code exists
-		$referrer = $this->manager->getUserByReferralCode($code);
+		$referrer = JVB()->referrals()->getUserByReferralCode($code);
 		if (!$referrer) {
 			return $this->error('Invalid referral code', 'invalid_code', 400);
 		}
 
 		// Create the referral
 		$user = wp_get_current_user();
-		$result = $this->manager->createReferral($referrer->ID, $user_id, $code);
+		$result = JVB()->referrals()->createReferral($referrer->ID, $user_id, $code);
 
 		if ($result) {
 			return $this->success([
@@ -651,4 +726,459 @@
 
 		return $this->error('Failed to apply referral code', 'creation_failed', 500);
 	}
+
+	/**********************************
+	 * ADDITIONAL
+	 */
+	/**
+	 * Handle client CSV upload
+	 */
+	public function handleClientUpload(WP_REST_Request $request): WP_REST_Response
+	{
+		$files = $request->get_file_params();
+
+		if (!isset($files['file'])) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'No file uploaded'
+			], 400);
+		}
+
+		$file = $files['file'];
+
+		// Validate file type
+		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
+		if (!in_array($file['type'], $allowed_types)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File must be a CSV'
+			], 400);
+		}
+
+		// Validate file size (10MB max)
+		if ($file['size'] > 10 * 1024 * 1024) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File size exceeds 10MB limit'
+			], 400);
+		}
+
+		// Import using JaneAppClientImporter
+		$importer = new JaneAppClientImporter();
+		$default_role = get_option(BASE . 'client_import_role', JVB_USER);
+
+		$options = [
+			'update_existing' => true,
+			'send_welcome_email' => false,
+			'create_users' => true,
+			'default_role' => $default_role
+		];
+
+		$result = $importer->importFromCSV($file['tmp_name'], $options);
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $result->get_error_message()
+			], 500);
+		}
+
+		// Build detailed message
+		$message = sprintf(
+			'Import complete: %d created, %d updated, %d skipped',
+			$result['created'],
+			$result['updated'],
+			$result['skipped']
+		);
+
+		$details = [];
+		if (!empty($result['skipped_details'])) {
+			$details = $result['skipped_details'];
+		}
+
+		// Clear cache
+		$this->cache->clear();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => $message,
+			'stats' => $result,
+			'skipped_details' => $details
+		]);
+	}
+
+	/**
+	 * Handle sales CSV upload
+	 */
+	public function handleSalesUpload(WP_REST_Request $request): WP_REST_Response
+	{
+		$files = $request->get_file_params();
+
+		if (!isset($files['file'])) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'No file uploaded'
+			], 400);
+		}
+
+		$file = $files['file'];
+
+		// Validate file type
+		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
+		if (!in_array($file['type'], $allowed_types)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File must be a CSV'
+			], 400);
+		}
+
+		// Validate file size (10MB max)
+		if ($file['size'] > 10 * 1024 * 1024) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File size exceeds 10MB limit'
+			], 400);
+		}
+
+		// Import using JaneSalesImporter
+		$importer = new JaneSalesImporter();
+		$options = [
+			'skip_existing' => true
+		];
+
+		$result = $importer->importFromCSV($file['tmp_name'], $options);
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $result->get_error_message()
+			], 500);
+		}
+
+		// Clear cache
+		$this->cache->clear();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Sales imported successfully',
+			'stats' => $result
+		]);
+	}
+
+	/**
+	 * Get referrals list for table display
+	 */
+	public function getReferralsList(WP_REST_Request $request): WP_REST_Response
+	{
+		$page = $request->get_param('page') ?: 1;
+		$per_page = $request->get_param('per_page') ?: 20;
+		$orderby = $request->get_param('orderby') ?: 'referred_at';
+		$order = strtoupper($request->get_param('order')) === 'ASC' ? 'ASC' : 'DESC';
+		$status = $request->get_param('status') ?: '';
+		$search = $request->get_param('search') ?: '';
+
+		$offset = ($page - 1) * $per_page;
+
+		// Build WHERE clause
+		$where_clauses = [];
+		$where_params = [];
+
+		if (!empty($status)) {
+			$where_clauses[] = "r.status = %s";
+			$where_params[] = $status;
+		}
+
+		if (!empty($search)) {
+			$where_clauses[] = "(r.referee_name LIKE %s OR r.referee_email LIKE %s OR referrer.display_name LIKE %s)";
+			$search_term = '%' . $this->wpdb->esc_like($search) . '%';
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+		}
+
+		$where = !empty($where_clauses) ? ' WHERE ' . implode(' AND ', $where_clauses) : '';
+
+		// Sanitize orderby to prevent SQL injection
+		$allowed_orderby = ['referred_at', 'consulted_at', 'treated_at', 'status', 'referee_name', 'referrer_name'];
+		if (!in_array($orderby, $allowed_orderby)) {
+			$orderby = 'referred_at';
+		}
+
+		// Get referrals with user info
+		$query = "SELECT
+			r.*,
+			referrer.display_name as referrer_name,
+			referrer.user_email as referrer_email,
+			referee.display_name as referee_display_name,
+			referee.user_email as referee_display_email,
+			(SELECT COUNT(*) FROM {$this->referrals_table} WHERE referrer_id = r.referrer_id) as referrer_total_referrals,
+			(SELECT SUM(amount) FROM {$this->rewards_table} WHERE user_id = r.referrer_id AND status = 'available') as referrer_available_rewards
+		FROM {$this->referrals_table} r
+		LEFT JOIN {$this->wpdb->users} referrer ON r.referrer_id = referrer.ID
+		LEFT JOIN {$this->wpdb->users} referee ON r.referee_id = referee.ID
+		{$where}
+		ORDER BY {$orderby} {$order}
+		LIMIT %d OFFSET %d";
+
+		$where_params[] = $per_page;
+		$where_params[] = $offset;
+
+		$prepared_query = $this->wpdb->prepare($query, $where_params);
+		$referrals = $this->wpdb->get_results($prepared_query);
+
+		// Get total count
+		$count_query = "SELECT COUNT(*) FROM {$this->referrals_table} r
+			LEFT JOIN {$this->wpdb->users} referrer ON r.referrer_id = referrer.ID
+			{$where}";
+
+		$total = $this->wpdb->get_var(
+			!empty($where_params) && count($where_params) > 2 ?
+				$this->wpdb->prepare($count_query, array_slice($where_params, 0, -2)) :
+				$count_query
+		);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'referrals' => $referrals,
+			'total' => (int)$total,
+			'page' => (int)$page,
+			'per_page' => (int)$per_page,
+			'total_pages' => ceil($total / $per_page)
+		]);
+	}
+
+	/**
+	 * Get details for a specific referral
+	 */
+	public function getReferralDetails(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = $request->get_param('id');
+
+		$referral = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT r.*,
+				referrer.display_name as referrer_name,
+				referee.display_name as referee_display_name
+			FROM {$this->referrals_table} r
+			LEFT JOIN {$this->wpdb->users} referrer ON r.referrer_id = referrer.ID
+			LEFT JOIN {$this->wpdb->users} referee ON r.referee_id = referee.ID
+			WHERE r.id = %d",
+			$referral_id
+		));
+
+		if (!$referral) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Referral not found'
+			], 404);
+		}
+
+		// Get associated treatments
+		$treatments = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT * FROM {$this->treatments_table} WHERE referral_id = %d ORDER BY treatment_date DESC",
+			$referral_id
+		));
+
+		// Get associated rewards
+		$rewards = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT * FROM {$this->rewards_table} WHERE referral_id = %d",
+			$referral_id
+		));
+
+		return new WP_REST_Response([
+			'success' => true,
+			'referral' => $referral,
+			'treatments' => $treatments,
+			'rewards' => $rewards
+		]);
+	}
+
+	/**
+	 * Handle manual mark as consulted
+	 */
+	public function handleMarkConsulted(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = $request->get_param('referral_id');
+
+		if (!$referral_id) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Referral ID required'
+			], 400);
+		}
+
+		$referral = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
+			$referral_id
+		));
+
+		if (!$referral) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Referral not found'
+			], 404);
+		}
+
+		if ($referral->status !== 'pending') {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Referral is not pending'
+			], 400);
+		}
+
+		// Update to consulted
+		$this->wpdb->update(
+			$this->referrals_table,
+			[
+				'status' => 'consulted',
+				'consulted_at' => current_time('mysql')
+			],
+			['id' => $referral_id],
+			['%s', '%s'],
+			['%d']
+		);
+
+		// Create consultation reward (20% off)
+		$this->wpdb->insert(
+			$this->rewards_table,
+			[
+				'referral_id' => $referral_id,
+				'user_id' => $referral->referee_id,
+				'reward_type' => 'referee',
+				'amount' => 20,
+				'reward_calculation' => 'percentage',
+				'status' => 'available',
+				'created_at' => current_time('mysql'),
+				'notes' => 'Consultation reward - 20% off first treatment'
+			],
+			['%d', '%d', '%s', '%f', '%s', '%s', '%s', '%s']
+		);
+
+		// Clear cache
+		$this->cache->clear();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Marked as consulted and reward created'
+		]);
+	}
+
+	/**
+	 * Handle manual mark as treated
+	 */
+	public function handleMarkTreated(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = $request->get_param('referral_id');
+
+		if (!$referral_id) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Referral ID required'
+			], 400);
+		}
+
+		$referral = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
+			$referral_id
+		));
+
+		if (!$referral) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Referral not found'
+			], 404);
+		}
+
+		if ($referral->status === 'treated') {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Referral already marked as treated'
+			], 400);
+		}
+
+		// Update to treated
+		$this->wpdb->update(
+			$this->referrals_table,
+			[
+				'status' => 'treated',
+				'treated_at' => current_time('mysql'),
+				'treatment_count' => ($referral->treatment_count ?? 0) + 1
+			],
+			['id' => $referral_id],
+			['%s', '%s', '%d'],
+			['%d']
+		);
+
+		// Create full rewards for both parties
+		$settings = JVB()->referrals()->getRewardSettings();
+
+		// Referrer reward
+		$this->wpdb->insert(
+			$this->rewards_table,
+			[
+				'referral_id' => $referral_id,
+				'user_id' => $referral->referrer_id,
+				'reward_type' => 'referrer',
+				'amount' => $settings['referrer_reward_amount'],
+				'reward_calculation' => $settings['referrer_reward_type'],
+				'status' => 'available',
+				'created_at' => current_time('mysql'),
+				'notes' => 'Referral reward for completed treatment'
+			],
+			['%d', '%d', '%s', '%f', '%s', '%s', '%s', '%s']
+		);
+
+		// Referee reward
+		$this->wpdb->insert(
+			$this->rewards_table,
+			[
+				'referral_id' => $referral_id,
+				'user_id' => $referral->referee_id,
+				'reward_type' => 'referee',
+				'amount' => $settings['referee_reward_amount'],
+				'reward_calculation' => $settings['referee_reward_type'],
+				'status' => 'available',
+				'created_at' => current_time('mysql'),
+				'notes' => 'Treatment completion reward'
+			],
+			['%d', '%d', '%s', '%f', '%s', '%s', '%s', '%s']
+		);
+
+		// Clear cache
+		$this->cache->clear();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Marked as treated and rewards created'
+		]);
+	}
+
+	/**
+	 * Get current user's referral stats
+	 */
+	public function getMyStats(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+		$stats = JVB()->referrals()->getUserStats($user_id);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'stats' => $stats
+		]);
+	}
+
+	/**
+	 * Get current user's referrals
+	 */
+	public function getMyReferrals(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+		$limit = $request->get_param('limit') ?: 20;
+
+		$referrals = JVB()->referrals()->getUserReferrals($user_id, ['limit' => $limit]);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'referrals' => $referrals
+		]);
+	}
 }

--
Gitblit v1.10.0