<?php
|
namespace JVBase\importers;
|
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* JaneApp Client List Importer
|
*
|
* Imports client data from JaneApp CSV exports and maps to WordPress users
|
*/
|
class JaneAppClientImporter
|
{
|
protected $wpdb;
|
protected string $jane_clients_table;
|
protected array $import_stats = [];
|
protected int $lineNumber = 0;
|
protected array $skipped_details = [];
|
// CSV column mapping
|
protected array $column_map = [
|
'patient_guid' => 'patient_guid',
|
'first_name' => 'First Name',
|
'last_name' => 'Last Name',
|
'email' => 'Email',
|
];
|
protected array $headers = [];
|
|
public function __construct()
|
{
|
global $wpdb;
|
$this->wpdb = $wpdb;
|
$this->jane_clients_table = $wpdb->prefix . BASE . 'jane_clients';
|
}
|
|
/**
|
* Import client list from CSV file
|
*
|
* @param string $file_path Path to the CSV file
|
* @param array $options Import options (e.g., update_existing, send_welcome_email)
|
* @return array|WP_Error Import results with stats and errors
|
*/
|
public function importFromCSV(string $file_path, array $options = []): array|WP_Error
|
{
|
$this->skipped_details = [];
|
$this->lineNumber = 0; // Reset line number
|
|
// Initialize stats
|
$this->import_stats = [
|
'total_rows' => 0,
|
'processed' => 0,
|
'created' => 0,
|
'updated' => 0,
|
'skipped' => 0,
|
'errors' => [],
|
'unmatched_emails' => [],
|
'skipped_details' => [] // Add this
|
];
|
|
// Validate file exists
|
if (!file_exists($file_path)) {
|
return new WP_Error('file_not_found', 'CSV file not found');
|
}
|
|
// Parse options
|
$update_existing = $options['update_existing'] ?? true;
|
$send_welcome_email = $options['send_welcome_email'] ?? false;
|
$create_users = $options['create_users'] ?? true;
|
|
// Open and parse CSV
|
$handle = fopen($file_path, 'r');
|
if (!$handle) {
|
return new WP_Error('file_open_error', 'Could not open CSV file');
|
}
|
|
// Get header row
|
$headers = fgetcsv($handle);
|
if (!$headers) {
|
fclose($handle);
|
return new WP_Error('invalid_csv', 'CSV file is empty or invalid');
|
}
|
$this->headers = array_map('trim', $headers);
|
|
// Map column indices
|
$column_indices = $this->mapColumnIndices($this->headers);
|
if (is_wp_error($column_indices)) {
|
fclose($handle);
|
return $column_indices;
|
}
|
|
// Start transaction for data integrity
|
$this->wpdb->query('START TRANSACTION');
|
|
try {
|
// Process each row
|
while (($row = fgetcsv($handle)) !== false) {
|
$this->import_stats['total_rows']++;
|
$this->lineNumber++;
|
|
$result = $this->processClientRow($row, $column_indices, [
|
'update_existing' => $update_existing,
|
'send_welcome_email' => $send_welcome_email,
|
'create_users' => $create_users,
|
'default_role' => $options['default_role'] ?? null
|
]);
|
|
if (is_wp_error($result)) {
|
$this->import_stats['errors'][] = [
|
'row' => $this->import_stats['total_rows'],
|
'error' => $result->get_error_message()
|
];
|
$this->import_stats['skipped']++;
|
} else {
|
$this->import_stats['processed']++;
|
if ($result['action'] === 'created') {
|
$this->import_stats['created']++;
|
} elseif ($result['action'] === 'updated') {
|
$this->import_stats['updated']++;
|
}
|
}
|
}
|
|
// Commit transaction
|
$this->wpdb->query('COMMIT');
|
|
} catch (\Exception $e) {
|
// Rollback on error
|
$this->wpdb->query('ROLLBACK');
|
fclose($handle);
|
return new WP_Error('import_error', 'Import failed: ' . $e->getMessage());
|
}
|
|
fclose($handle);
|
|
// Add skipped details to stats
|
$this->import_stats['skipped_details'] = $this->skipped_details;
|
|
return $this->import_stats;
|
}
|
|
/**
|
* Map CSV column headers to internal field names
|
*
|
* @param array $headers CSV header row
|
* @return array|WP_Error Column index mapping or error
|
*/
|
protected function mapColumnIndices(array $headers): array|WP_Error
|
{
|
$indices = [];
|
|
foreach ($this->column_map as $field => $csv_column) {
|
$index = array_search($csv_column, $headers);
|
if ($index === false) {
|
return new WP_Error(
|
'missing_column',
|
sprintf('Required column "%s" not found in CSV', $csv_column)
|
);
|
}
|
$indices[$field] = $index;
|
}
|
|
return $indices;
|
}
|
|
/**
|
* Process a single client row from CSV
|
*
|
* @param array $row CSV row data
|
* @param array $column_indices Column mapping
|
* @param array $options Processing options
|
* @return array|WP_Error Result of processing
|
*/
|
protected function processClientRow(array $row, array $column_indices, array $options): array|WP_Error
|
{
|
// Extract data from row
|
$patient_guid = trim($row[$column_indices['patient_guid']] ?? '');
|
$first_name = trim($row[$column_indices['first_name']] ?? '');
|
$last_name = trim($row[$column_indices['last_name']] ?? '');
|
$email = trim($row[$column_indices['email']] ?? '');
|
|
// Validate required fields
|
if (empty($patient_guid) || empty($email)) {
|
$this->skipped_details[] = [
|
'name' => $first_name . ' ' . $last_name,
|
'guid' => $patient_guid,
|
'email' => $email,
|
'reason' => 'Missing guid or email',
|
'line' => $this->lineNumber
|
];
|
return new WP_Error('invalid_data', 'Missing patient_guid or email');
|
}
|
|
// Sanitize email
|
$email = sanitize_email($email);
|
if (!is_email($email)) {
|
$this->skipped_details[] = [
|
'name' => $first_name . ' ' . $last_name,
|
'guid' => $patient_guid,
|
'email' => $email,
|
'reason' => 'Invalid Email',
|
'line' => $this->lineNumber
|
];
|
return new WP_Error('invalid_email', 'Invalid email address: ' . $email);
|
}
|
|
// Check if client already exists in mapping table
|
$existing_mapping = $this->getClientByGuid($patient_guid);
|
|
// Build full data array with all CSV columns for meta storage
|
$data = [];
|
foreach ($row as $index => $value) {
|
$header = $this->headers[$index] ?? 'unknown_' . $index;
|
$data[$header] = trim($value);
|
}
|
|
// Ensure these keys exist for backward compatibility
|
$data['patient_guid'] = $patient_guid;
|
$data['First Name'] = $first_name;
|
$data['Last Name'] = $last_name;
|
$data['Email'] = $email;
|
|
// Find or create WordPress user
|
$user = get_user_by('email', $email);
|
$action = 'existing';
|
|
if ($user) {
|
if ($options['update_existing']) {
|
$this->updateExistingClient($user->ID, $data, $options);
|
$action = 'updated';
|
} else {
|
$this->skipped_details[] = [
|
'name' => $first_name . ' ' . $last_name,
|
'email' => $email,
|
'reason' => 'User already exists (update not enabled)',
|
'line' => $this->lineNumber
|
];
|
return new WP_Error('user_exists', 'User already exists');
|
}
|
} elseif ($options['create_users']) {
|
// Create new user
|
$user_id = $this->createClientUser($data, $options);
|
|
if (is_wp_error($user_id)) {
|
$this->skipped_details[] = [
|
'name' => $first_name . ' ' . $last_name,
|
'guid' => $patient_guid,
|
'email' => $email,
|
'reason' => $user_id->get_error_message(),
|
'line' => $this->lineNumber
|
];
|
return $user_id;
|
}
|
|
$user = get_user_by('ID', $user_id);
|
$action = 'created';
|
} else {
|
// User doesn't exist and we're not creating users
|
$this->skipped_details[] = [
|
'name' => $first_name . ' ' . $last_name,
|
'guid' => $patient_guid,
|
'email' => $email,
|
'reason' => 'User not found and create_users is false',
|
'line' => $this->lineNumber
|
];
|
$this->import_stats['unmatched_emails'][] = $email;
|
return new WP_Error('user_not_found', 'User not found and create_users is false');
|
}
|
|
// Update or insert client mapping
|
if ($existing_mapping) {
|
if ($options['update_existing']) {
|
$this->updateClientMapping($existing_mapping->id, [
|
'user_id' => $user->ID,
|
'first_name' => $first_name,
|
'last_name' => $last_name,
|
'email' => $email
|
]);
|
}
|
} else {
|
$this->insertClientMapping([
|
'patient_guid' => $patient_guid,
|
'user_id' => $user->ID,
|
'first_name' => $first_name,
|
'last_name' => $last_name,
|
'email' => $email
|
]);
|
}
|
|
return [
|
'action' => $action,
|
'user_id' => $user->ID,
|
'patient_guid' => $patient_guid
|
];
|
}
|
|
/**
|
* Create a new client user from Jane App data
|
*
|
* @param array $data Client data from CSV
|
* @param array $options Import options
|
* @return int|WP_Error User ID or error
|
*/
|
protected function createClientUser(array $data, array $options)
|
{
|
$email = sanitize_email($data['Email']);
|
$first_name = sanitize_text_field($data['First Name'] ?? '');
|
$last_name = sanitize_text_field($data['Last Name'] ?? '');
|
|
// Generate username from email
|
$username = sanitize_user($email);
|
|
// Ensure unique username
|
$base_username = $username;
|
$counter = 1;
|
while (username_exists($username)) {
|
$username = $base_username . $counter;
|
$counter++;
|
}
|
|
// Get the role from options with proper fallback
|
$role = $options['default_role'] ?? get_option(BASE . 'client_import_role', BASE.'client');
|
|
// Ensure role exists
|
if (!get_role($role)) {
|
return new WP_Error('invalid_role', 'Invalid role');
|
}
|
|
// Create user
|
$user_data = [
|
'user_login' => $username,
|
'user_email' => $email,
|
'first_name' => $first_name,
|
'last_name' => $last_name,
|
'display_name' => trim($first_name . ' ' . $last_name),
|
'role' => $role,
|
'user_pass' => wp_generate_password(16, true, true)
|
];
|
|
$user_id = wp_insert_user($user_data);
|
|
if (is_wp_error($user_id)) {
|
return $user_id;
|
}
|
|
// Store Jane App data as user meta
|
$this->storeClientMeta($user_id, $data);
|
|
// Send welcome email if enabled
|
if ($options['send_welcome_email'] ?? false) {
|
wp_new_user_notification($user_id, null, 'user');
|
}
|
|
return $user_id;
|
}
|
|
/**
|
* Store Jane App client data as user meta
|
*
|
* @param int $user_id
|
* @param array $data
|
*/
|
protected function storeClientMeta(int $user_id, array $data): void
|
{
|
// Store Jane App specific fields
|
if (!empty($data['patient_guid'])) {
|
update_user_meta($user_id, BASE . 'jane_patient_guid', sanitize_text_field($data['patient_guid']));
|
}
|
|
if (!empty($data['Patient Number'])) {
|
update_user_meta($user_id, BASE . 'jane_patient_number', sanitize_text_field($data['Patient Number']));
|
}
|
|
if (!empty($data['Member Since'])) {
|
update_user_meta($user_id, BASE . 'member_since', sanitize_text_field($data['Member Since']));
|
}
|
|
if (!empty($data['Mobile Phone'])) {
|
update_user_meta($user_id, BASE . 'phone', sanitize_text_field($data['Mobile Phone']));
|
}
|
|
if (!empty($data['Birth Date'])) {
|
update_user_meta($user_id, BASE . 'birth_date', sanitize_text_field($data['Birth Date']));
|
}
|
|
if (!empty($data['Referral Source'])) {
|
update_user_meta($user_id, BASE . 'referral_source', sanitize_text_field($data['Referral Source']));
|
}
|
|
// Store full Jane App data as JSON for reference
|
update_user_meta($user_id, BASE . 'jane_import_data', $data);
|
update_user_meta($user_id, BASE . 'jane_import_date', current_time('mysql'));
|
}
|
|
/**
|
* Update existing client with Jane App data
|
*
|
* @param int $user_id
|
* @param array $data
|
* @param array $options
|
*/
|
protected function updateExistingClient(int $user_id, array $data, array $options): void
|
{
|
// Update user fields if they're empty
|
$user_data = ['ID' => $user_id];
|
|
$current_user = get_user_by('ID', $user_id);
|
|
if (empty($current_user->first_name) && !empty($data['First Name'])) {
|
$user_data['first_name'] = sanitize_text_field($data['First Name']);
|
}
|
|
if (empty($current_user->last_name) && !empty($data['Last Name'])) {
|
$user_data['last_name'] = sanitize_text_field($data['Last Name']);
|
}
|
|
if (count($user_data) > 1) {
|
wp_update_user($user_data);
|
}
|
|
// Always update meta data
|
$this->storeClientMeta($user_id, $data);
|
}
|
|
/**
|
* Generate unique username from email
|
*
|
* @param string $email Email address
|
* @return string Unique username
|
*/
|
protected function generateUsername(string $email): string
|
{
|
$base_username = sanitize_user(substr($email, 0, strpos($email, '@')));
|
$username = $base_username;
|
$counter = 1;
|
|
while (username_exists($username)) {
|
$username = $base_username . $counter;
|
$counter++;
|
}
|
|
return $username;
|
}
|
|
/**
|
* Get client by patient GUID
|
*
|
* @param string $patient_guid Patient GUID
|
* @return object|null Client data or null
|
*/
|
protected function getClientByGuid(string $patient_guid): ?object
|
{
|
return $this->wpdb->get_row($this->wpdb->prepare(
|
"SELECT * FROM {$this->jane_clients_table} WHERE patient_guid = %s",
|
$patient_guid
|
));
|
}
|
|
/**
|
* Get client by user ID
|
*
|
* @param int $user_id WordPress user ID
|
* @return object|null Client data or null
|
*/
|
public function getClientByUserId(int $user_id): ?object
|
{
|
return $this->wpdb->get_row($this->wpdb->prepare(
|
"SELECT * FROM {$this->jane_clients_table} WHERE user_id = %d",
|
$user_id
|
));
|
}
|
|
/**
|
* Insert new client mapping
|
*
|
* @param array $data Client data
|
* @return int|false Insert ID or false on failure
|
*/
|
protected function insertClientMapping(array $data): int|false
|
{
|
$result = $this->wpdb->insert(
|
$this->jane_clients_table,
|
$data,
|
['%s', '%d', '%s', '%s', '%s']
|
);
|
|
return $result ? $this->wpdb->insert_id : false;
|
}
|
|
/**
|
* Update existing client mapping
|
*
|
* @param int $id Mapping ID
|
* @param array $data Updated data
|
* @return bool Success
|
*/
|
protected function updateClientMapping(int $id, array $data): bool
|
{
|
return (bool) $this->wpdb->update(
|
$this->jane_clients_table,
|
$data,
|
['id' => $id],
|
['%d', '%s', '%s', '%s'],
|
['%d']
|
);
|
}
|
|
/**
|
* Get user ID by patient GUID
|
*
|
* @param string $patient_guid Patient GUID
|
* @return int|null User ID or null if not found
|
*/
|
public function getUserIdByGuid(string $patient_guid): ?int
|
{
|
$client = $this->getClientByGuid($patient_guid);
|
return $client ? (int) $client->user_id : null;
|
}
|
|
/**
|
* Get import statistics
|
*
|
* @return array Import statistics
|
*/
|
public function getImportStats(): array
|
{
|
return $this->import_stats;
|
}
|
}
|