Jake Vanderwerf
4 days ago 747d741293e064a979d7bf6c143ef969ea6d7629
inc/rest/routes/ReferralRoutes.php
@@ -1,10 +1,12 @@
<?php
namespace JVBase\rest\routes;
use JVBase\base\Site;
use JVBase\importers\JaneAppClientImporter;
use JVBase\managers\JaneSalesImporter;
use JVBase\managers\MagicLinkManager;
use JVBase\rest\RestRouteManager;
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;
@@ -16,130 +18,80 @@
/**
 * REST API routes for referral system
 */
class ReferralRoutes extends RestRouteManager
class ReferralRoutes extends Rest
{
   protected string $referrals_table;
   protected string $rewards_table;
   protected $wpdb;
   public function __construct()
   {
      $this->route = 'referrals';
      $this->cache_name = 'referrals';
      $this->cacheName = 'referrals';
      $this->cacheTtl = (int)HOUR_IN_SECONDS;
      parent::__construct();
      global $wpdb;
      $this->wpdb = $wpdb;
      $this->referrals_table = $wpdb->prefix . BASE . 'referrals';
      $this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards';
      add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
   }
   public function registerRoutes(): void
   {
      /**
       * Main referrals endpoint
       * GET: List referrals with filters
       * POST: Perform actions (invite, consulted, treated, remove, resend)
       */
      register_rest_route($this->namespace, "/{$this->route}", [
         [
            'methods' => 'GET',
            'callback' => [$this, 'getReferrals'],
            'permission_callback' => [$this, 'checkPermission'],
            'args' => [
               'user' => ['type' => 'integer', 'sanitize_callback' => 'absint'],
               'status' => ['type' => 'string', 'enum' => ['all', 'pending', 'consulted', 'treated', 'unused', 'registered', 'completed']],
               'date_start' => ['type' => 'string'],
               'date_end' => ['type' => 'string'],
               'limit' => ['type' => 'integer', 'default' => 50],
               'offset' => ['type' => 'integer', 'default' => 0],
               'format' => ['type' => 'string', 'enum' => ['simple', 'formatted'], 'default' => 'formatted'],
               'search' => ['type' => 'string']
            ]
         ],
         [
            'methods' => 'POST',
            'callback' => [$this, 'handleAction'],
            'permission_callback' => [$this, 'checkPermission'],
            'args' => [
               'action' => [
                  'required' => true,
                  'type' => 'string',
                  'enum' => ['invite', 'consulted', 'treated', 'remove', 'resend']
               ]
            ]
         ]
      ]);
      // 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();
      /**
       * Referral code endpoint
       * GET: Get user's referral code
       * POST: Validate a referral code
       */
      register_rest_route($this->namespace, "/{$this->route}/code", [
         [
            'methods' => 'GET',
            'callback' => [$this, 'getCode'],
            'permission_callback' => [$this, 'checkPermission'],
            'args' => [
               'user' => ['type' => 'integer', 'sanitize_callback' => 'absint']
            ]
         ],
         [
            'methods' => 'POST',
            'callback' => [$this, 'validateCode'],
            'permission_callback' => '__return_true', // Public endpoint
            'args' => [
               'code' => ['required' => true, 'type' => 'string']
            ]
         ]
      ]);
      // 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();
      /**
       * Stats endpoint
       * GET: Get user's referral statistics
       */
      register_rest_route($this->namespace, "/{$this->route}/stats", [
         'methods' => 'GET',
         'callback' => [$this, 'getStats'],
         'permission_callback' => [$this, 'checkPermission'],
         'args' => [
            'user' => ['type' => 'integer', 'sanitize_callback' => 'absint'],
         ]
      ]);
      // Stats endpoint
      Route::for('referrals/stats')
         ->get([$this, 'getStats'])
         ->args(['user' => 'integer'])
         ->auth('user')
         ->rateLimit(30)
         ->register();
      /**
       * Settings endpoint (admin only)
       */
      register_rest_route($this->namespace, "/{$this->route}/settings", [
         [
            'methods' => 'GET',
            'callback' => [$this, 'getSettings'],
            'permission_callback' => [$this, 'checkAdminPermission']
         ],
         [
            'methods' => 'POST',
            'callback' => [$this, 'updateSettings'],
            'permission_callback' => [$this, 'checkAdminPermission']
         ]
      ]);
      // Settings endpoint (admin only)
      Route::for('referrals/settings')
         ->get([$this, 'getSettings'])
         ->post([$this, 'updateSettings'])
         ->auth('admin')
         ->rateLimit(10)
         ->register();
      /**
       * CSV Upload endpoints (admin only)
       */
      register_rest_route($this->namespace, "/{$this->route}/upload-clients", [
         'methods' => 'POST',
         'callback' => [$this, 'handleClientUpload'],
         'permission_callback' => [$this, 'checkAdminPermission']
      ]);
      // CSV Upload endpoints (admin only)
      Route::for('referrals/upload-clients')
         ->post([$this, 'handleClientUpload'])
         ->auth('admin')
         ->rateLimit(3)
         ->register();
      register_rest_route($this->namespace, "/{$this->route}/upload-sales", [
         'methods' => 'POST',
         'callback' => [$this, 'handleSalesUpload'],
         'permission_callback' => [$this, 'checkAdminPermission']
      ]);
      Route::for('referrals/upload-sales')
         ->post([$this, 'handleSalesUpload'])
         ->auth('admin')
         ->rateLimit(3)
         ->register();
   }
   /**
@@ -152,19 +104,16 @@
   {
      $user_id = $request->get_param('user');
      // Determine scope
      // Determine scope: admin without user param gets all referrals
      if (!$user_id) {
         $current_user_id = get_current_user_id();
         $is_admin = current_user_can('manage_options');
         if ($is_admin) {
            // Admin with no user param = get all referrals
         if (current_user_can('manage_options')) {
            return $this->getAllReferrals($request);
         }
         $user_id = $current_user_id;
      }
      // Get user's referrals
      // Build cache key
      $args = [
         'status' => $request->get_param('status') ?? 'all',
         'limit' => $request->get_param('limit') ?? 50,
@@ -172,14 +121,15 @@
         'date_start' => $request->get_param('date_start'),
         'date_end' => $request->get_param('date_end'),
      ];
      $cache_key = "user_{$user_id}_" . $this->cache->generateKey($args);
      $cache_key = "ref_{$user_id}_" . md5(serialize($args));
      // Check headers for 304 Not Modified
      // Check 304 Not Modified
      $cache_check = $this->checkHeaders($request, $cache_key);
      if ($cache_check instanceof WP_REST_Response) {
         return $cache_check; // Returns 304 if not modified
         return $cache_check;
      }
      // Get referrals from manager
      $referrals = JVB()->referrals()->getUserReferrals($user_id, $args);
      $data = [
@@ -187,11 +137,8 @@
         'total' => count($referrals)
      ];
      // Create response with cache headers
      $response = $this->success($data);
      // Add ETag and Last-Modified headers
      return $this->addCacheHeaders($response, $cache_key, $data);
      return $this->addCacheHeaders($response);
   }
   /**
@@ -217,51 +164,53 @@
    */
   protected function actionInvite(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_params();
      error_log('Send Referral Invitations:'.print_r($data, true));
      $user = absint($request->get_param('user'));
      if (!$this->checkUser($user)) {
         return new WP_REST_Response([
            'success'   => false,
            'message'   => 'No user found'
         ]);
      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 invitation format
      foreach ($invitations as $key => $invite) {
         if (!array_key_exists('name', $invite) || !array_key_exists('email', $invite)) {
            unset($invitations[$key]);
         } else {
            $temp = [
               'name'   => sanitize_text_field($invite['name']),
               'email'  => sanitize_email($invite['email'])
      // 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'])
            ];
            $invitations[$key] = $temp;
         }
      }
      if (empty($sanitized_invitations)) {
         return $this->error('No valid invitations provided', 'no_invitations', 400);
      }
      $operationID = sanitize_text_field($request->get_param('id'));
      $operation = JVB()->queue()->queueOperation(
      JVB()->queue()->queueOperation(
         'referral_invite',
         $user,
         [
            'subject' => $subject,
            'message' => $message,
            'invitations' => $invitations
            'invitations' => $sanitized_invitations
         ],
         [
            'operation_id' => $operationID
         ]
         ['operation_id' => $operationID]
      );
      return new WP_REST_Response([
         'success'   => true,
         'message'   => 'Queued for Processing',
         'operation' => $operationID
      ]);
      return $this->queued($operationID, 'Referral invitations queued');
   }
   /**
@@ -270,7 +219,7 @@
   protected function actionUpdateStatus(WP_REST_Request $request, string $status): WP_REST_Response
   {
      if (!current_user_can('manage_options')) {
         return $this->error('Admin permission required', 'unauthorized', 403);
         return $this->forbidden('Admin permission required');
      }
      $referral_id = $request->get_param('referral_id');
@@ -278,42 +227,16 @@
         return $this->error('referral_id required', 'missing_id', 400);
      }
      $referral = $this->wpdb->get_row($this->wpdb->prepare(
         "SELECT * FROM {$this->referrals_table} WHERE id = %d",
         $referral_id
      ));
      $result = JVB()->referrals()->updateStatus($referral_id, $status);
      if (!$referral) {
         return $this->error('Referral not found', 'not_found', 404);
      if (is_wp_error($result)) {
         $code = $result->get_error_code();
         return $code === 'not_found'
            ? $this->notFound($result->get_error_message())
            : $this->error($result->get_error_message(), $code, 500);
      }
      // 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->wpdb->update(
         $this->referrals_table,
         $update_data,
         ['id' => $referral_id],
         array_fill(0, count($update_data), '%s'),
         ['%d']
      );
      if ($updated) {
         // Also create rewards if treated
         if ($status === 'treated') {
            $this->createRewards($referral);
         }
      }
      $this->cache->clear();
      $this->cache->flush();
      return $this->success(['message' => "Referral marked as {$status}"]);
   }
@@ -327,29 +250,20 @@
         return $this->error('referral_id required', 'missing_id', 400);
      }
      $referral = $this->wpdb->get_row($this->wpdb->prepare(
         "SELECT * FROM {$this->referrals_table} WHERE id = %d",
         $referral_id
      ));
      $result = JVB()->referrals()->removeReferral($referral_id, get_current_user_id());
      if (!$referral) {
         return $this->error('Referral not found', 'not_found', 404);
      if (is_wp_error($result)) {
         $code = $result->get_error_code();
         $status = match($code) {
            'not_found'      => 404,
            'unauthorized'   => 403,
            'invalid_status' => 400,
            default          => 500
         };
         return $this->error($result->get_error_message(), $code, $status);
      }
      // Check ownership
      $current_user_id = get_current_user_id();
      if ($referral->referrer_id != $current_user_id && !current_user_can('manage_options')) {
         return $this->error('Unauthorized', 'unauthorized', 403);
      }
      // Can only remove pending referrals
      if ($referral->status !== 'pending') {
         return $this->error('Can only remove pending referrals', 'invalid_status', 400);
      }
      $this->wpdb->delete($this->referrals_table, ['id' => $referral_id], ['%d']);
      $this->cache->clear();
      $this->cache->flush();
      return $this->success(['message' => 'Referral removed']);
   }
@@ -363,45 +277,18 @@
         return $this->error('referral_id required', 'missing_id', 400);
      }
      $current_user_id = get_current_user_id();
      $referral = $this->wpdb->get_row($this->wpdb->prepare(
         "SELECT * FROM {$this->referrals_table} WHERE id = %d AND referrer_id = %d",
         $referral_id,
         $current_user_id
      ));
      if (!$referral) {
         return $this->error('Referral not found', 'not_found', 404);
      }
      // 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!'
      );
      $result = JVB()->referrals()->resendInvitation($referral_id, get_current_user_id());
      if (is_wp_error($result)) {
         return $this->error($result->get_error_message(), 'send_failed', 500);
         $code = $result->get_error_code();
         $status = match($code) {
            'not_found'  => 404,
            'rate_limit' => 429,
            default      => 500
         };
         return $this->error($result->get_error_message(), $code, $status);
      }
      // Set rate limit
      set_transient($transient_key, time(), WEEK_IN_SECONDS);
      return $this->success(['message' => 'Invitation resent']);
   }
@@ -415,7 +302,7 @@
      // Check permission
      if ($user_id != get_current_user_id() && !current_user_can('manage_options')) {
         return $this->error('Unauthorized', 'unauthorized', 403);
         return $this->forbidden('Unauthorized');
      }
      $code = JVB()->referrals()->getUserReferralCode($user_id);
@@ -466,11 +353,10 @@
    */
   public function getStats(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = $request->get_param('user');
      $user_id = $request->get_param('user') ?? get_current_user_id();
      $cache_key = "stats_{$user_id}";
      // Check for 304 Not Modified
      // Check 304 Not Modified
      $cache_check = $this->checkHeaders($request, $cache_key);
      if ($cache_check instanceof WP_REST_Response) {
         return $cache_check;
@@ -479,9 +365,7 @@
      $stats = JVB()->referrals()->getUserStats($user_id);
      $response = $this->success(['items' => [$stats]]);
      // Add cache headers (5 minutes for stats)
      return $this->addCacheHeaders($response, $cache_key, $stats, 5 * MINUTE_IN_SECONDS);
      return $this->addCacheHeaders($response);
   }
   /**
@@ -508,7 +392,7 @@
      ];
      update_option(BASE . 'referral_settings', $settings);
      $this->cache->clear();
      $this->cache->flush();
      return $this->success([
         'message' => 'Settings updated',
@@ -521,106 +405,18 @@
    */
   protected function getAllReferrals(WP_REST_Request $request): WP_REST_Response
   {
      $where = ['1=1'];
      $where_params = [];
      $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 = '%' . $this->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 OR ru.display_name LIKE %s OR ru.user_email LIKE %s)';
         $where_params[] = $search_term;
         $where_params[] = $search_term;
         $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;
      $query = "SELECT r.*, u.display_name as referrer_name
         FROM {$this->referrals_table} r
         LEFT JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
         WHERE " . implode(' AND ', $where) . "
         ORDER BY referred_at DESC
         LIMIT %d OFFSET %d";
      $items = $this->wpdb->get_results($this->wpdb->prepare($query, $where_params));
      error_log('All Referrals result: '.print_r($items, true));
      return $this->success([
         'items' => $items,
         'total' => count($items)
      $items = JVB()->referrals()->getAllReferrals([
         'status'     => $request->get_param('status') ?? 'all',
         'search'     => $request->get_param('search'),
         'date_start' => $request->get_param('date_start'),
         'date_end'   => $request->get_param('date_end'),
         'limit'      => $request->get_param('limit') ?? 50,
         'offset'     => $request->get_param('offset') ?? 0,
      ]);
      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->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')
         ],
         ['%d', '%d', '%s', '%f', '%s', '%s', '%s']
      );
      // Referee reward
      if ($referral->referee_id) {
         $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')
            ],
            ['%d', '%d', '%s', '%f', '%s', '%s', '%s']
         );
      }
   }
   /**
    * Check admin permission
    */
   public function checkAdminPermission(WP_REST_Request $request): bool
   {
      return current_user_can('manage_options') && parent::checkPermission($request);
   }
   /**
    * Process queued referral operations
@@ -637,22 +433,22 @@
         $data['subject'],
         $data['message']
      );
      if ($result['success']) {
         $this->cache->clear();
         $this->cache->flush();
      }
      // Build summary message
      $textResult = 'Sent invitations. ';
      $textResult .= 'Success: ' . count($result['result']['success']) . '. ';
      $textResult .= 'Failed: ' . count($result['result']['failed']) . '.';
      return [
         'success'   => true,
         'message'   => $textResult,
         'details'   => [
         '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'])
            'failed' => $result['result']['failed'],
            'total' => count($data['invitations'])
         ]
      ];
   }
@@ -662,22 +458,14 @@
    */
   public function handleClientUpload(WP_REST_Request $request): WP_REST_Response
   {
      // Access files from $_FILES directly for REST API uploads
      if (empty($_FILES['file'])) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'No file uploaded'
         ], 400);
         return $this->error('No file uploaded', 'no_file', 400);
      }
      $file = $_FILES['file'];
      // Check for upload errors
      if ($file['error'] !== UPLOAD_ERR_OK) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'File upload error: ' . $file['error']
         ], 400);
         return $this->error('File upload error: ' . $file['error'], 'upload_error', 400);
      }
      // Validate file type
@@ -687,23 +475,17 @@
      finfo_close($finfo);
      if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'File must be a CSV'
         ], 400);
         return $this->error('File must be a CSV', 'invalid_file_type', 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);
         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', JVB_USER);
      $default_role = get_option(BASE . 'referral_role', Site::getDefaultReferralRole());
      $options = [
         'update_existing' => true,
@@ -715,33 +497,20 @@
      $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);
         return $this->error($result->get_error_message(), 'import_failed', 500);
      }
      // Build detailed message
      $message = sprintf(
         'Import complete: %d created, %d updated, %d skipped',
         $result['created'],
         $result['updated'],
         $result['skipped']
      );
      $this->cache->flush();
      $details = [];
      if (!empty($result['skipped_details'])) {
         $details = $result['skipped_details'];
      }
      // Clear cache
      $this->cache->clear();
      return new WP_REST_Response([
         'success' => true,
         'message' => $message,
      return $this->success([
         'message' => sprintf(
            'Import complete: %d created, %d updated, %d skipped',
            $result['created'],
            $result['updated'],
            $result['skipped']
         ),
         'stats' => $result,
         'skipped_details' => $details
         'skipped_details' => $result['skipped_details'] ?? []
      ]);
   }
@@ -750,22 +519,14 @@
    */
   public function handleSalesUpload(WP_REST_Request $request): WP_REST_Response
   {
      // Access files from $_FILES directly for REST API uploads
      if (empty($_FILES['file'])) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'No file uploaded'
         ], 400);
         return $this->error('No file uploaded', 'no_file', 400);
      }
      $file = $_FILES['file'];
      // Check for upload errors
      if ($file['error'] !== UPLOAD_ERR_OK) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'File upload error: ' . $file['error']
         ], 400);
         return $this->error('File upload error: ' . $file['error'], 'upload_error', 400);
      }
      // Validate file type
@@ -775,40 +536,25 @@
      finfo_close($finfo);
      if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'File must be a CSV'
         ], 400);
         return $this->error('File must be a CSV', 'invalid_file_type', 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);
         return $this->error('File size exceeds 10MB limit', 'file_too_large', 400);
      }
      // Import using JaneSalesImporter
      $importer = new JaneSalesImporter();
      $options = [
         'skip_existing' => true
      ];
      $result = $importer->importFromCSV($file['tmp_name'], $options);
      // Import using JaneAppSalesImporter
      $importer = new JaneAppSalesImporter();
      $result = $importer->importFromCSV($file['tmp_name'], ['skip_existing' => true]);
      if (is_wp_error($result)) {
         return new WP_REST_Response([
            'success' => false,
            'message' => $result->get_error_message()
         ], 500);
         return $this->error($result->get_error_message(), 'import_failed', 500);
      }
      // Clear cache
      $this->cache->clear();
      $this->cache->flush();
      return new WP_REST_Response([
         'success' => true,
      return $this->success([
         'message' => 'Sales imported successfully',
         'stats' => $result
      ]);