cacheName = 'referrals'; $this->cacheTtl = (int)HOUR_IN_SECONDS; parent::__construct(); $this->cache = Cache::for($this->cacheName, 'custom', $this->cacheTtl)->user(); } public function registerRoutes(): void { // Main referrals endpoint - list and manage referrals Route::for('referrals') ->get([$this, 'getReferrals']) ->args([ 'status' => 'string|enum:all,invited,pending,consulted,treated,cancelled|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(); // Referral code endpoint Route::for('referrals/code') ->get([$this, 'getCode']) ->auth('user') ->rateLimit(30) ->post([$this, 'validateCode']) ->args(['code' => 'string|required']) ->auth('public') ->rateLimit(10) ->register(); // Stats endpoint Route::for('referrals/stats') ->get([$this, 'getStats']) ->auth('user') ->rateLimit(30) ->register(); // Settings endpoint (admin only) Route::for('referrals/settings') ->get([$this, 'getSettings']) ->post([$this, 'updateSettings']) ->auth('admin') ->rateLimit(10) ->register(); // CSV Upload endpoints (admin only) Route::for('referrals/upload-clients') ->post([$this, 'handleClientUpload']) ->auth('admin') ->rateLimit(3) ->register(); Route::for('referrals/upload-sales') ->post([$this, 'handleSalesUpload']) ->auth('admin') ->rateLimit(3) ->register(); } /** * GET /referrals * Get referrals with optional filters * - User gets their own referrals * - Admin with no user param gets all referrals */ public function getReferrals(WP_REST_Request $request): WP_REST_Response { $user_id = get_current_user_id(); if (!$user_id) { return $this->unauthorized(); } if (current_user_can('manage_options')) { return $this->getAllReferrals($request); } $args = $this->buildArgs($request); // Get referrals from manager $referrals = Referrals::getInstance()->getUserReferrals($user_id, $args); $response = $this->success($referrals); 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 = get_current_user_id(); if (!$user) { return $this->unauthorized(); } $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')); Queue::getInstance()->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); } [$success, $message] = Referrals::getInstance()->updateStatus($referral_id, $status); if (!$success) { return $message === 'Referral not found' ? $this->notFound($message) : $this->error($message); } $this->cache->flush(); return $this->success(['message' => "Referral marked as {$status}"]); } /** * 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); } $result = Referrals::getInstance()->removeReferral($referral_id, get_current_user_id()); if (!$result['success']) { return $this->error($result['message']); } 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); } $result = Referrals::getInstance()->resendInvitation($referral_id, get_current_user_id()); if (!$result['success']) { return $this->error($result['message']); } return $this->success(['message' => 'Invitation resent']); } /** * GET /referrals/code * Get user's referral code */ public function getCode(WP_REST_Request $request): WP_REST_Response { $user_id = get_current_user_id(); if (!$user_id) { return $this->unauthorized(); } $code = Referrals::getInstance()->getUserReferralCode($user_id); if (is_wp_error($code)) { return $this->error($code->get_error_message(), 'code_error', 400); } return $this->success([ 'code' => $code, 'share_url' => home_url('/?ref=' . $code) ]); } /** * POST /referrals/code * Validate a referral code */ public function validateCode(WP_REST_Request $request): WP_REST_Response { $code = strtoupper(sanitize_text_field($request->get_param('code')??'')); if (empty($code)) { return $this->error('Code required', 'missing_code', 400); } $referrer = Referrals::getInstance()->getUserByReferralCode($code); 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'); } return $this->success([ 'valid' => true, 'code' => $code, 'referrer_name' => $referrer->display_name ]); } /** * GET /referrals/stats * Get user's referral statistics */ public function getStats(WP_REST_Request $request): WP_REST_Response { $user_id = get_current_user_id(); if (!$user_id) { return $this->unauthorized(); } $stats = Referrals::getInstance()->getUserStats($user_id); return $this->success(['items' => [$stats]]); } /** * GET /referrals/settings */ public function getSettings(WP_REST_Request $request): WP_REST_Response { $settings = Referrals::getInstance()->getRewardSettings(); return $this->success(['settings' => $settings]); } /** * POST /referrals/settings */ public function updateSettings(WP_REST_Request $request): WP_REST_Response { $settings = [ '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 $this->success([ 'message' => 'Settings updated', 'settings' => $settings ]); } /** * Helper: Get all referrals (admin only) */ protected function buildArgs(WP_REST_Request $request):array { $params = $request->get_params(); $args = [ 'limit' => absint($params['limit']??20), 'offset' => absint($params['offset']??0) ]; if (array_key_exists('status', $params) && $params['status'] !== 'all') { $args['status'] = sanitize_text_field($params['status']); } if (array_key_exists('search', $params)) { $args['search'] = sanitize_text_field($params['search']); } if (array_key_exists('date_start', $params) && array_key_exists('date_end', $params)) { $args['created_at'] = ['BETWEEN', [sanitize_text_field($params['date_start']), sanitize_text_field($params['date_end'])]]; } return $args; } protected function getAllReferrals(WP_REST_Request $request): WP_REST_Response { $args = $this->buildArgs($request); $items = Referrals::getInstance()->getReferrals($args); return $this->success($items); } /** * 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::getReferralRole()); $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 ]); } }