From 48721c85ebcfa973ee81719d2467ca80e4253dc9 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 01 May 2026 17:30:03 +0000
Subject: [PATCH] =Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5

---
 inc/rest/routes/ReferralRoutes.php |  800 ++++++++++++++++++++++++++++++++++++++++++---------------
 1 files changed, 588 insertions(+), 212 deletions(-)

diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index d7f097f..2ef284a 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -1,7 +1,12 @@
 <?php
 namespace JVBase\rest\routes;
 
-use JVBase\rest\RestRouteManager;
+use JVBase\base\Site;
+use JVBase\importers\JaneAppClientImporter;
+use JVBase\importers\JaneAppSalesImporter;
+use JVBase\managers\CustomTable;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
 use WP_REST_Request;
 use WP_REST_Response;
 use WP_Error;
@@ -13,301 +18,672 @@
 /**
  * REST API routes for referral system
  */
-class ReferralRoutes extends RestRouteManager
+class ReferralRoutes extends Rest
 {
+	protected CustomTable $referrals;
+	protected CustomTable $rewards;
+	protected CustomTable $treatments;
 
 	public function __construct()
 	{
-		$this->route = 'referrals';
+		$this->cacheName = 'referrals';
+		$this->cacheTtl = (int)HOUR_IN_SECONDS;
 		parent::__construct();
+
+		$this->referrals = CustomTable::for('referrals');
+		$this->rewards = CustomTable::for('referral_rewards');
+		$this->treatments = CustomTable::for('referral_treatments');
+
+		add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
 	}
 
 	public function registerRoutes(): void
 	{
-		// Get user's referrals
-		register_rest_route($this->namespace, "/{$this->route}", [
-			'methods' => 'GET',
-			'callback' => [$this, 'getUserReferrals'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
+		// Main referrals endpoint - list and manage referrals
+		Route::for('referrals')
+			->get([$this, 'getReferrals'])
+			->args([
+				'user' => 'integer',
+				'status' => 'string|enum:all,pending,consulted,treated,unused,registered,completed|default:all',
+				'date_start' => 'string',
+				'date_end' => 'string',
+				'limit' => 'integer|default:50',
+				'offset' => 'integer|default:0',
+				'search' => 'string'
+			])
+			->rateLimit()
+			->post([$this, 'handleAction'])
+			->args([
+				'action' => 'string|required|enum:invite,consulted,treated,remove,resend'
+			])
+			->auth('user')
+			->rateLimit(10)
+			->register();
 
-		// Get or create referral code
-		register_rest_route($this->namespace, "/{$this->route}/code", [
-			'methods' => 'GET',
-			'callback' => [$this, 'getReferralCode'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
+		// Referral code endpoint
+		Route::for('referrals/code')
+			->get([$this, 'getCode'])
+			->args(['user' => 'integer'])
+			->auth('user')
+			->rateLimit(30)
+			->post([$this, 'validateCode'])
+			->args(['code' => 'string|required'])
+			->auth('public')
+			->rateLimit(10)
+			->register();
 
-		// Update referral code
-		register_rest_route($this->namespace, "/{$this->route}/code", [
-			'methods' => 'POST',
-			'callback' => [$this, 'updateReferralCode'],
-			'permission_callback' => [$this, 'checkPermission'],
-			'args' => [
-				'code' => [
-					'required' => true,
-					'type' => 'string',
-					'validate_callback' => function($param) {
-						return preg_match('/^[A-Z0-9]+$/i', $param);
-					}
-				]
-			]
-		]);
+		// Stats endpoint
+		Route::for('referrals/stats')
+			->get([$this, 'getStats'])
+			->args(['user' => 'integer'])
+			->auth('user')
+			->rateLimit(30)
+			->register();
 
-		// Track referral click (public endpoint)
-		register_rest_route($this->namespace, "/{$this->route}/track", [
-			'methods' => 'POST',
-			'callback' => [$this, 'trackReferralClick'],
-			'permission_callback' => '__return_true',
-			'args' => [
-				'code' => [
-					'required' => true,
-					'type' => 'string'
-				]
-			]
-		]);
+		// Settings endpoint (admin only)
+		Route::for('referrals/settings')
+			->get([$this, 'getSettings'])
+			->post([$this, 'updateSettings'])
+			->auth('admin')
+			->rateLimit(10)
+			->register();
 
-		// Mark referral as treated
-		register_rest_route($this->namespace, "/{$this->route}/(?P<id>\d+)/treat", [
-			'methods' => 'POST',
-			'callback' => [$this, 'markAsTreated'],
-			'permission_callback' => function() {
-				return current_user_can('manage_options');
-			},
-			'args' => [
-				'id' => [
-					'required' => true,
-					'validate_callback' => function($param) {
-						return is_numeric($param);
-					}
-				]
-			]
-		]);
+		// CSV Upload endpoints (admin only)
+		Route::for('referrals/upload-clients')
+			->post([$this, 'handleClientUpload'])
+			->auth('admin')
+			->rateLimit(3)
+			->register();
 
-		// Get user stats
-		register_rest_route($this->namespace, "/{$this->route}/stats", [
-			'methods' => 'GET',
-			'callback' => [$this, 'getUserStats'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
-
-		// Get top referrers (admin only)
-		register_rest_route($this->namespace, "/{$this->route}/leaderboard", [
-			'methods' => 'GET',
-			'callback' => [$this, 'getTopReferrers'],
-			'permission_callback' => function() {
-				return current_user_can('manage_options');
-			},
-			'args' => [
-				'period' => [
-					'default' => 'week',
-					'enum' => ['day', 'week', 'month', 'all']
-				],
-				'limit' => [
-					'default' => 10,
-					'type' => 'integer'
-				]
-			]
-		]);
-
-		// Get/Update referral settings (admin only)
-		register_rest_route($this->namespace, "/{$this->route}/settings", [
-			[
-				'methods' => 'GET',
-				'callback' => [$this, 'getSettings'],
-				'permission_callback' => function() {
-					return current_user_can('manage_options');
-				}
-			],
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'updateSettings'],
-				'permission_callback' => function() {
-					return current_user_can('manage_options');
-				}
-			]
-		]);
-	}
-
-	public function checkPermission(WP_REST_Request $request): bool
-	{
-		return is_user_logged_in();
+		Route::for('referrals/upload-sales')
+			->post([$this, 'handleSalesUpload'])
+			->auth('admin')
+			->rateLimit(3)
+			->register();
 	}
 
 	/**
-	 * Get user's referrals
+	 * GET /referrals
+	 * Get referrals with optional filters
+	 * - User gets their own referrals
+	 * - Admin with no user param gets all referrals
 	 */
-	public function getUserReferrals(WP_REST_Request $request): WP_REST_Response
+	public function getReferrals(WP_REST_Request $request): WP_REST_Response
 	{
-		$user_id = get_current_user_id();
+		$user_id = $request->get_param('user');
 
+		// Determine scope: admin without user param gets all referrals
+		if (!$user_id) {
+			$current_user_id = get_current_user_id();
+			if (current_user_can('manage_options')) {
+				return $this->getAllReferrals($request);
+			}
+			$user_id = $current_user_id;
+		}
+
+		// Build cache key
 		$args = [
 			'status' => $request->get_param('status') ?? 'all',
 			'limit' => $request->get_param('limit') ?? 50,
-			'offset' => $request->get_param('offset') ?? 0
+			'offset' => $request->get_param('offset') ?? 0,
+			'date_start' => $request->get_param('date_start'),
+			'date_end' => $request->get_param('date_end'),
 		];
+		$cache_key = "user_{$user_id}_" . $this->cache->generateKey($args);
 
+		// Check 304 Not Modified
+		$cache_check = $this->checkHeaders($request, $cache_key);
+		if ($cache_check instanceof WP_REST_Response) {
+			return $cache_check;
+		}
+
+		// Get referrals from manager
 		$referrals = JVB()->referrals()->getUserReferrals($user_id, $args);
 
-		return new WP_REST_Response([
-			'success' => true,
-			'referrals' => $referrals
-		]);
+		$data = [
+			'items' => $referrals,
+			'total' => count($referrals)
+		];
+
+		$response = $this->success($data);
+		return $this->addCacheHeaders($response);
 	}
 
 	/**
+	 * POST /referrals
+	 * Handle various referral actions based on 'action' parameter
+	 */
+	public function handleAction(WP_REST_Request $request): WP_REST_Response
+	{
+		$action = $request->get_param('action');
+
+		return match($action) {
+			'invite' => $this->actionInvite($request),
+			'consulted' => $this->actionUpdateStatus($request, 'consulted'),
+			'treated' => $this->actionUpdateStatus($request, 'treated'),
+			'remove' => $this->actionRemove($request),
+			'resend' => $this->actionResend($request),
+			default => $this->error('Invalid action', 'invalid_action', 400)
+		};
+	}
+
+	/**
+	 * Action: Send batch referral invitations
+	 */
+	protected function actionInvite(WP_REST_Request $request): WP_REST_Response
+	{
+		$user = absint($request->get_param('user'));
+		if (!$user || !get_userdata($user)) {
+			return $this->error('Invalid user', 'invalid_user', 400);
+		}
+
+		//Additional check to not send too many emails in an hour
+		$user = absint($request->get_param('user'));
+		$transient_key = "referral_invite_limit_{$user}";
+		$recent_invites = get_transient($transient_key) ?: 0;
+
+		if ($recent_invites >= 20) { // Max 5 batch invites per hour
+			return $this->error('Too many invitations sent. Please try again later.', 'rate_limit', 429);
+		}
+		set_transient($transient_key, $recent_invites + 1, HOUR_IN_SECONDS);
+
+		$subject = sanitize_text_field($request->get_param('subject'));
+		$message = sanitize_textarea_field($request->get_param('message'));
+		$invitations = $request->get_param('invite');
+
+		// Validate and sanitize invitations
+		$sanitized_invitations = [];
+		foreach ($invitations as $invite) {
+			if (isset($invite['name'], $invite['email'])) {
+				$sanitized_invitations[] = [
+					'name' => sanitize_text_field($invite['name']),
+					'email' => sanitize_email($invite['email'])
+				];
+			}
+		}
+
+		if (empty($sanitized_invitations)) {
+			return $this->error('No valid invitations provided', 'no_invitations', 400);
+		}
+
+		$operationID = sanitize_text_field($request->get_param('id'));
+		JVB()->queue()->queueOperation(
+			'referral_invite',
+			$user,
+			[
+				'subject' => $subject,
+				'message' => $message,
+				'invitations' => $sanitized_invitations
+			],
+			['operation_id' => $operationID]
+		);
+
+		return $this->queued($operationID, 'Referral invitations queued');
+	}
+
+	/**
+	 * Action: Update referral status (admin only)
+	 */
+	protected function actionUpdateStatus(WP_REST_Request $request, string $status): WP_REST_Response
+	{
+		if (!current_user_can('manage_options')) {
+			return $this->forbidden('Admin permission required');
+		}
+
+		$referral_id = $request->get_param('referral_id');
+		if (!$referral_id) {
+			return $this->error('referral_id required', 'missing_id', 400);
+		}
+
+		// Get referral using CustomTable
+		$referral = $this->referrals->get(['id' => $referral_id]);
+		if (!$referral) {
+			return $this->notFound('Referral not found');
+		}
+
+		// Update status
+		$update_data = ['status' => $status];
+		$update_data["{$status}_at"] = current_time('mysql');
+
+		if ($status === 'treated') {
+			$update_data['treatment_count'] = ($referral->treatment_count ?? 0) + 1;
+		}
+
+		$updated = $this->referrals->update($update_data, ['id' => $referral_id]);
+
+		if ($updated !== false) {
+			// Create rewards if treated
+			if ($status === 'treated') {
+				$this->createRewards($referral);
+			}
+
+			$this->cache->flush();
+			return $this->success(['message' => "Referral marked as {$status}"]);
+		}
+
+		return $this->error('Failed to update referral', 'update_failed', 500);
+	}
+
+	/**
+	 * Action: Remove referral
+	 */
+	protected function actionRemove(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = $request->get_param('referral_id');
+		if (!$referral_id) {
+			return $this->error('referral_id required', 'missing_id', 400);
+		}
+
+		// Get referral
+		$referral = $this->referrals->get(['id' => $referral_id]);
+		if (!$referral) {
+			return $this->notFound('Referral not found');
+		}
+
+		// Check ownership
+		$current_user_id = get_current_user_id();
+		if ($referral->referrer_id != $current_user_id && !current_user_can('manage_options')) {
+			return $this->forbidden('Unauthorized');
+		}
+
+		// Can only remove pending referrals
+		if ($referral->status !== 'pending') {
+			return $this->error('Can only remove pending referrals', 'invalid_status', 400);
+		}
+
+		$this->referrals->delete(['id' => $referral_id]);
+		$this->cache->flush();
+
+		return $this->success(['message' => 'Referral removed']);
+	}
+
+	/**
+	 * Action: Resend invitation
+	 */
+	protected function actionResend(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = $request->get_param('referral_id');
+		if (!$referral_id) {
+			return $this->error('referral_id required', 'missing_id', 400);
+		}
+
+		$current_user_id = get_current_user_id();
+
+		// Get referral with ownership check
+		$referral = $this->referrals->where([
+			'id' => $referral_id,
+			'referrer_id' => $current_user_id
+		])->first();
+
+		if (!$referral) {
+			return $this->notFound('Referral not found');
+		}
+
+		// Check rate limit (once per week)
+		$transient_key = 'referral_last_invite_' . md5($referral->referee_email);
+		$last_invite = get_transient($transient_key);
+
+		if ($last_invite && (time() - $last_invite) < WEEK_IN_SECONDS) {
+			return $this->error(
+				'Can only resend once per week',
+				'rate_limit',
+				429
+			);
+		}
+
+		// Resend via referral manager
+		$result = JVB()->referrals()->sendReferralInvitation(
+			$current_user_id,
+			$referral->referee_email,
+			$referral->referee_name,
+			sprintf('Reminder: Join %s', get_bloginfo('name')),
+			'Just a friendly reminder about my invitation!'
+		);
+
+		if (is_wp_error($result)) {
+			return $this->error($result->get_error_message(), 'send_failed', 500);
+		}
+
+		set_transient($transient_key, time(), WEEK_IN_SECONDS);
+		return $this->success(['message' => 'Invitation resent']);
+	}
+
+	/**
+	 * GET /referrals/code
 	 * Get user's referral code
 	 */
-	public function getReferralCode(WP_REST_Request $request): WP_REST_Response
+	public function getCode(WP_REST_Request $request): WP_REST_Response
 	{
-		$user_id = get_current_user_id();
+		$user_id = $request->get_param('user') ?? get_current_user_id();
+
+		// Check permission
+		if ($user_id != get_current_user_id() && !current_user_can('manage_options')) {
+			return $this->forbidden('Unauthorized');
+		}
+
 		$code = JVB()->referrals()->getUserReferralCode($user_id);
 
 		if (is_wp_error($code)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => $code->get_error_message()
-			], 400);
+			return $this->error($code->get_error_message(), 'code_error', 400);
 		}
 
-		return new WP_REST_Response([
-			'success' => true,
+		return $this->success([
 			'code' => $code,
 			'share_url' => home_url('/?ref=' . $code)
 		]);
 	}
 
 	/**
-	 * Update user's referral code
+	 * POST /referrals/code
+	 * Validate a referral code
 	 */
-	public function updateReferralCode(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
-		$new_code = strtoupper(sanitize_text_field($request->get_param('code')));
-
-		$result = JVB()->referrals()->getUserReferralCode($user_id, $new_code);
-
-		if (is_wp_error($result)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => $result->get_error_message()
-			], 400);
-		}
-
-		return new WP_REST_Response([
-			'success' => true,
-			'code' => $result,
-			'message' => 'Referral code updated successfully'
-		]);
-	}
-
-	/**
-	 * Track referral click and store in session
-	 */
-	public function trackReferralClick(WP_REST_Request $request): WP_REST_Response
+	public function validateCode(WP_REST_Request $request): WP_REST_Response
 	{
 		$code = strtoupper(sanitize_text_field($request->get_param('code')));
 
-		// Start session if not already started
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
+		if (empty($code)) {
+			return $this->error('Code required', 'missing_code', 400);
 		}
 
-		// Store referral code in both session and cookie (30 day expiry)
-		$_SESSION[BASE . 'referral_code'] = $code;
-		setcookie(BASE . 'referral_code', $code, time() + (30 * DAY_IN_SECONDS), '/');
+		$referrer = JVB()->referrals()->getUserByReferralCode($code);
 
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Referral tracked'
+		if (!$referrer) {
+			return $this->error('Invalid referral code', 'invalid_code', 404);
+		}
+
+		// Check self-referral
+		if (is_user_logged_in() && get_current_user_id() === $referrer->ID) {
+			return $this->error('Cannot use your own referral code', 'self_referral', 400);
+		}
+
+		return $this->success([
+			'valid' => true,
+			'code' => $code,
+			'referrer_name' => $referrer->display_name
 		]);
 	}
 
 	/**
-	 * Mark referral as treated
+	 * GET /referrals/stats
+	 * Get user's referral statistics
 	 */
-	public function markAsTreated(WP_REST_Request $request): WP_REST_Response
+	public function getStats(WP_REST_Request $request): WP_REST_Response
 	{
-		$referral_id = intval($request->get_param('id'));
+		$user_id = $request->get_param('user') ?? get_current_user_id();
+		$cache_key = "stats_{$user_id}";
 
-		$result = JVB()->referrals()->markAsTreated($referral_id, true);
-
-		if (!$result) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Failed to update referral'
-			], 400);
+		// Check 304 Not Modified
+		$cache_check = $this->checkHeaders($request, $cache_key);
+		if ($cache_check instanceof WP_REST_Response) {
+			return $cache_check;
 		}
 
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Referral marked as treated and rewards created'
-		]);
-	}
-
-	/**
-	 * Get user stats
-	 */
-	public function getUserStats(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
-		]);
+		$response = $this->success(['items' => [$stats]]);
+		return $this->addCacheHeaders($response);
 	}
 
 	/**
-	 * Get top referrers
-	 */
-	public function getTopReferrers(WP_REST_Request $request): WP_REST_Response
-	{
-		$period = $request->get_param('period') ?? 'week';
-		$limit = $request->get_param('limit') ?? 10;
-
-		$top_referrers = JVB()->referrals()->getTopReferrers($limit, $period);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'period' => $period,
-			'referrers' => $top_referrers
-		]);
-	}
-
-	/**
-	 * Get referral settings
+	 * GET /referrals/settings
 	 */
 	public function getSettings(WP_REST_Request $request): WP_REST_Response
 	{
-		$settings = get_option(BASE . 'referral_settings', []);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'settings' => $settings
-		]);
+		$settings = JVB()->referrals()->getRewardSettings();
+		return $this->success(['settings' => $settings]);
 	}
 
 	/**
-	 * Update referral settings
+	 * POST /referrals/settings
 	 */
 	public function updateSettings(WP_REST_Request $request): WP_REST_Response
 	{
 		$settings = [
-			'referrer_reward_type' => $request->get_param('referrer_reward_type') ?? 'per_user',
+			'referrer_reward_type' => $request->get_param('referrer_reward_type') ?? 'fixed',
 			'referrer_reward_amount' => floatval($request->get_param('referrer_reward_amount') ?? 25),
+			'referrer_reward_applies_to' => $request->get_param('referrer_reward_applies_to') ?? 'per_user',
 			'referee_reward_type' => $request->get_param('referee_reward_type') ?? 'percentage',
 			'referee_reward_amount' => floatval($request->get_param('referee_reward_amount') ?? 20),
 			'referee_reward_applies_to' => $request->get_param('referee_reward_applies_to') ?? 'first_order'
 		];
 
 		update_option(BASE . 'referral_settings', $settings);
+		$this->cache->flush();
 
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Settings updated successfully',
+		return $this->success([
+			'message' => 'Settings updated',
 			'settings' => $settings
 		]);
 	}
+
+	/**
+	 * Helper: Get all referrals (admin only)
+	 */
+	protected function getAllReferrals(WP_REST_Request $request): WP_REST_Response
+	{
+		global $wpdb;
+
+		$where = ['1=1'];
+		$where_params = [];
+
+		// Build WHERE conditions
+		$status = $request->get_param('status');
+		if ($status && $status !== 'all') {
+			$where[] = 'status = %s';
+			$where_params[] = $status;
+		}
+
+		if ($date_start = $request->get_param('date_start')) {
+			$where[] = 'referred_at >= %s';
+			$where_params[] = $date_start;
+		}
+
+		if ($date_end = $request->get_param('date_end')) {
+			$where[] = 'referred_at <= %s';
+			$where_params[] = $date_end;
+		}
+
+		$search = $request->get_param('search');
+		if (!empty($search)) {
+			$search_term = '%' . $wpdb->esc_like($search) . '%';
+			$where[] = '(r.referee_name LIKE %s OR r.referee_email LIKE %s OR r.referral_code LIKE %s OR u.display_name LIKE %s)';
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+		}
+
+		$limit = $request->get_param('limit') ?? 50;
+		$offset = $request->get_param('offset') ?? 0;
+
+		$where_params[] = $limit;
+		$where_params[] = $offset;
+
+		// Use CustomTable's query method
+		$query = "SELECT r.*, u.display_name as referrer_name
+			FROM {table} r
+			LEFT JOIN {$wpdb->users} u ON r.referrer_id = u.ID
+			WHERE " . implode(' AND ', $where) . "
+			ORDER BY referred_at DESC
+			LIMIT %d OFFSET %d";
+
+		$items = $this->referrals->queryResults($query, $where_params);
+
+		return $this->success([
+			'items' => $items,
+			'total' => count($items)
+		]);
+	}
+
+	/**
+	 * Helper: Create rewards for completed referral
+	 */
+	protected function createRewards(object $referral): void
+	{
+		$settings = JVB()->referrals()->getRewardSettings();
+
+		// Referrer reward
+		$this->rewards->insert([
+			'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'
+		]);
+
+		// Referee reward
+		if ($referral->referee_id) {
+			$this->rewards->insert([
+				'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'
+			]);
+		}
+	}
+
+	/**
+	 * Process queued referral operations
+	 */
+	public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error
+	{
+		if ($operation->type !== 'referral_invite') {
+			return $result;
+		}
+
+		$result = JVB()->referrals()->sendBatchReferralInvitations(
+			$operation->user_id,
+			$data['invitations'],
+			$data['subject'],
+			$data['message']
+		);
+
+		if ($result['success']) {
+			$this->cache->flush();
+		}
+
+		return [
+			'success' => true,
+			'message' => sprintf(
+				'Sent invitations. Success: %d. Failed: %d.',
+				count($result['result']['success']),
+				count($result['result']['failed'])
+			),
+			'details' => [
+				'successful' => $result['result']['success'],
+				'failed' => $result['result']['failed'],
+				'total' => count($data['invitations'])
+			]
+		];
+	}
+
+	/**
+	 * Handle client CSV upload
+	 */
+	public function handleClientUpload(WP_REST_Request $request): WP_REST_Response
+	{
+		if (empty($_FILES['file'])) {
+			return $this->error('No file uploaded', 'no_file', 400);
+		}
+
+		$file = $_FILES['file'];
+
+		if ($file['error'] !== UPLOAD_ERR_OK) {
+			return $this->error('File upload error: ' . $file['error'], 'upload_error', 400);
+		}
+
+		// Validate file type
+		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
+		$finfo = finfo_open(FILEINFO_MIME_TYPE);
+		$mime_type = finfo_file($finfo, $file['tmp_name']);
+		finfo_close($finfo);
+
+		if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
+			return $this->error('File must be a CSV', 'invalid_file_type', 400);
+		}
+
+		// Validate file size (10MB max)
+		if ($file['size'] > 10 * 1024 * 1024) {
+			return $this->error('File size exceeds 10MB limit', 'file_too_large', 400);
+		}
+
+		// Import using JaneAppClientImporter
+		$importer = new JaneAppClientImporter();
+		$default_role = get_option(BASE . 'referral_role', Site::getDefaultReferralRole());
+
+		$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 $this->error($result->get_error_message(), 'import_failed', 500);
+		}
+
+		$this->cache->flush();
+
+		return $this->success([
+			'message' => sprintf(
+				'Import complete: %d created, %d updated, %d skipped',
+				$result['created'],
+				$result['updated'],
+				$result['skipped']
+			),
+			'stats' => $result,
+			'skipped_details' => $result['skipped_details'] ?? []
+		]);
+	}
+
+	/**
+	 * Handle sales CSV upload
+	 */
+	public function handleSalesUpload(WP_REST_Request $request): WP_REST_Response
+	{
+		if (empty($_FILES['file'])) {
+			return $this->error('No file uploaded', 'no_file', 400);
+		}
+
+		$file = $_FILES['file'];
+
+		if ($file['error'] !== UPLOAD_ERR_OK) {
+			return $this->error('File upload error: ' . $file['error'], 'upload_error', 400);
+		}
+
+		// Validate file type
+		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
+		$finfo = finfo_open(FILEINFO_MIME_TYPE);
+		$mime_type = finfo_file($finfo, $file['tmp_name']);
+		finfo_close($finfo);
+
+		if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
+			return $this->error('File must be a CSV', 'invalid_file_type', 400);
+		}
+
+		// Validate file size (10MB max)
+		if ($file['size'] > 10 * 1024 * 1024) {
+			return $this->error('File size exceeds 10MB limit', 'file_too_large', 400);
+		}
+
+		// Import using JaneAppSalesImporter
+		$importer = new JaneAppSalesImporter();
+		$result = $importer->importFromCSV($file['tmp_name'], ['skip_existing' => true]);
+
+		if (is_wp_error($result)) {
+			return $this->error($result->get_error_message(), 'import_failed', 500);
+		}
+
+		$this->cache->flush();
+
+		return $this->success([
+			'message' => 'Sales imported successfully',
+			'stats' => $result
+		]);
+	}
 }

--
Gitblit v1.10.0