post([$this, 'importClients']) ->args([ 'options' => 'string', // JSON string of options ]) ->auth('admin') ->rateLimit(3, 300) ->register(); // 3 imports per 5 minutes // Sales import endpoint Route::for('jane/import-sales') ->post([$this, 'importSales']) ->args([ 'options' => 'string', // JSON string of options ]) ->auth('admin') ->rateLimit(3, 300) ->register(); // 3 imports per 5 minutes // Get import status Route::for(Route::pattern('jane/import-status/{id}')) ->get([$this, 'getImportStatus']) ->arg('id', 'string|required') ->auth('admin') ->rateLimit(30) ->register(); } /** * Check if user has admin permissions */ public function checkAdminPermission(): bool { return current_user_can('manage_options'); } /** * Import clients from CSV * * @param WP_REST_Request $request * @return WP_REST_Response */ public function importClients(WP_REST_Request $request): WP_REST_Response { // Get uploaded file $files = $request->get_file_params(); if (empty($files['file'])) { return $this->error('No file uploaded', 'no_file', 400); } $file = $files['file']; // Validate file type if (!$this->isValidCSV($file)) { return $this->error('Invalid file type. Please upload a CSV file.', 'invalid_file', 400); } // Get and parse options $options_param = $request->get_param('options'); $options = !empty($options_param) ? json_decode($options_param, true) : []; $default_options = [ 'update_existing' => true, 'create_users' => true, 'send_welcome_email' => false ]; $options = wp_parse_args($options, $default_options); // Process import $importer = new JaneAppClientImporter(); $results = $importer->importFromCSV($file['tmp_name'], $options); if (is_wp_error($results)) { $this->logError('Client import failed', [ 'error' => $results->get_error_message(), 'file' => $file['name'] ]); return $this->error( $results->get_error_message(), 'import_failed', 500 ); } // Store results in transient for status checking $import_id = wp_generate_password(12, false); set_transient('jane_import_' . $import_id, [ 'type' => 'clients', 'results' => $results, 'completed_at' => current_time('mysql') ], HOUR_IN_SECONDS); return $this->success([ 'import_id' => $import_id, 'results' => $results, 'summary' => $this->generateClientImportSummary($results) ]); } /** * Import sales from CSV * * @param WP_REST_Request $request * @return WP_REST_Response */ public function importSales(WP_REST_Request $request): WP_REST_Response { // Get uploaded file $files = $request->get_file_params(); if (empty($files['file'])) { return $this->error('No file uploaded', 'no_file', 400); } $file = $files['file']; // Validate file type if (!$this->isValidCSV($file)) { return $this->error('Invalid file type. Please upload a CSV file.', 'invalid_file', 400); } // Get and parse options $options_param = $request->get_param('options'); $options = !empty($options_param) ? json_decode($options_param, true) : []; $default_options = [ 'skip_existing' => true ]; $options = wp_parse_args($options, $default_options); // Process import $importer = new JaneAppSalesImporter(); $results = $importer->importFromCSV($file['tmp_name'], $options); if (is_wp_error($results)) { $this->logError('Sales import failed', [ 'error' => $results->get_error_message(), 'file' => $file['name'] ]); return $this->error( $results->get_error_message(), 'import_failed', 500 ); } // Store results in transient for status checking $import_id = wp_generate_password(12, false); set_transient('jane_import_' . $import_id, [ 'type' => 'sales', 'results' => $results, 'completed_at' => current_time('mysql') ], HOUR_IN_SECONDS); return $this->success([ 'import_id' => $import_id, 'results' => $results, 'summary' => $this->generateSalesImportSummary($results) ]); } /** * Get import status by ID * * @param WP_REST_Request $request * @return WP_REST_Response */ public function getImportStatus(WP_REST_Request $request): WP_REST_Response { $import_id = sanitize_text_field($request->get_param('id')); $import_data = get_transient('jane_import_' . $import_id); if (!$import_data) { return $this->notFound('Import not found or expired'); } return $this->success($import_data); } /** * Validate CSV file * * @param array $file Uploaded file data * @return bool */ protected function isValidCSV(array $file): bool { // Check file extension $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if ($ext !== 'csv') { return false; } // Check MIME type $allowed_types = ['text/csv', 'text/plain', 'application/csv', 'application/vnd.ms-excel']; if (!in_array($file['type'], $allowed_types)) { return false; } // Check if file is actually readable as CSV $handle = fopen($file['tmp_name'], 'r'); if (!$handle) { return false; } $header = fgetcsv($handle); fclose($handle); return !empty($header); } /** * Generate human-readable summary of client import * * @param array $results Import results * @return string */ protected function generateClientImportSummary(array $results): string { $summary = []; if ($results['created'] > 0) { $summary[] = "{$results['created']} new users created"; } if ($results['updated'] > 0) { $summary[] = "{$results['updated']} existing users updated"; } if ($results['skipped'] > 0) { $summary[] = "{$results['skipped']} rows skipped"; } if (count($results['errors']) > 0) { $summary[] = count($results['errors']) . " errors encountered"; } if (count($results['unmatched_emails']) > 0) { $summary[] = count($results['unmatched_emails']) . " unmatched emails"; } return implode('. ', $summary) . '.'; } /** * Generate human-readable summary of sales import * * @param array $results Import results * @return string */ protected function generateSalesImportSummary(array $results): string { $summary = []; if ($results['consultations'] > 0) { $summary[] = "{$results['consultations']} consultations processed"; } if ($results['treatments'] > 0) { $summary[] = "{$results['treatments']} treatments recorded"; } if ($results['skipped'] > 0) { $summary[] = "{$results['skipped']} rows skipped"; } if (count($results['errors']) > 0) { $summary[] = count($results['errors']) . " errors encountered"; } if (count($results['unmatched_guids']) > 0) { $summary[] = count($results['unmatched_guids']) . " unmatched patient GUIDs"; } if (count($results['no_referral']) > 0) { $summary[] = count($results['no_referral']) . " users without referral records"; } return implode('. ', $summary) . '.'; } }