/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ CI = &get_instance(); // Load required libraries and models $this->CI->load->library('desk_moloni/moloni_api_client'); $this->CI->load->model('desk_moloni/desk_moloni_mapping_model', 'mapping_model'); $this->CI->load->model('desk_moloni/desk_moloni_sync_log_model', 'sync_log_model'); $this->CI->load->model('clients_model'); $this->api_client = $this->CI->moloni_api_client; $this->mapping_model = $this->CI->mapping_model; $this->sync_log_model = $this->CI->sync_log_model; } /** * Synchronize clients bidirectionally * * @param string $direction Sync direction: 'perfex_to_moloni', 'moloni_to_perfex', 'bidirectional' * @param array $options Sync options * @return array Sync result */ public function sync_bidirectional($direction = 'bidirectional', $options = []) { $results = []; try { switch ($direction) { case 'perfex_to_moloni': $results = $this->sync_perfex_to_moloni($options); break; case 'moloni_to_perfex': $results = $this->sync_moloni_to_perfex($options); break; case 'bidirectional': default: $results['perfex_to_moloni'] = $this->sync_perfex_to_moloni($options); $results['moloni_to_perfex'] = $this->sync_moloni_to_perfex($options); break; } return [ 'success' => true, 'direction' => $direction, 'results' => $results, 'timestamp' => date('Y-m-d H:i:s') ]; } catch (Exception $e) { return [ 'success' => false, 'direction' => $direction, 'error' => $this->sanitize_error_message($e->getMessage()), 'timestamp' => date('Y-m-d H:i:s') ]; } } /** * Sync from Perfex to Moloni */ private function sync_perfex_to_moloni($options = []) { $results = ['synced' => 0, 'failed' => 0, 'errors' => []]; // Get all clients that need syncing $clients_to_sync = $this->get_clients_needing_sync('perfex_to_moloni', $options); foreach ($clients_to_sync as $client_id) { $sync_result = $this->sync_client($client_id, $options); if ($sync_result['success']) { $results['synced']++; } else { $results['failed']++; $results['errors'][] = $sync_result['error']; } } return $results; } /** * Sync from Moloni to Perfex */ private function sync_moloni_to_perfex($options = []) { $results = ['synced' => 0, 'failed' => 0, 'errors' => []]; // Get all Moloni clients that need syncing to Perfex $moloni_clients = $this->get_moloni_clients_needing_sync($options); foreach ($moloni_clients as $moloni_client) { $sync_result = $this->create_or_update_perfex_client($moloni_client, $options); if ($sync_result['success']) { $results['synced']++; } else { $results['failed']++; $results['errors'][] = $sync_result['error']; } } return $results; } /** * Get clients that need syncing with batch processing support */ private function get_clients_needing_sync($direction, $options = []) { // Implementation with bulk sync and batch processing support $this->CI->db->select('userid'); $this->CI->db->from('tblclients'); if (isset($options['modified_since'])) { $this->CI->db->where('datemodified >', $options['modified_since']); } if (isset($options['client_ids'])) { $this->CI->db->where_in('userid', $options['client_ids']); } // Batch processing - limit results for bulk sync if (isset($options['batch_size'])) { $this->CI->db->limit($options['batch_size']); } // Add priority ordering $this->CI->db->order_by('datemodified', 'DESC'); $query = $this->CI->db->get(); return array_column($query->result_array(), 'userid'); } /** * Get Moloni clients that need syncing to Perfex */ private function get_moloni_clients_needing_sync($options = []) { // Mock implementation - would call real Moloni API return [ ['id' => 'mock_client_1', 'name' => 'Mock Client 1', 'email' => 'mock1@example.com'], ['id' => 'mock_client_2', 'name' => 'Mock Client 2', 'email' => 'mock2@example.com'] ]; } /** * Create or update Perfex client from Moloni data */ private function create_or_update_perfex_client($moloni_client, $options = []) { try { // Transform Moloni data to Perfex format $perfex_data = $this->customer_mapper->toPerfex($moloni_client); // Check if client already exists $existing_mapping = $this->mapping_model->get_by_moloni_id('client', $moloni_client['id']); if ($existing_mapping) { // Update existing client $result = $this->CI->clients_model->update($perfex_data, $existing_mapping['perfex_id']); $action = 'updated'; $client_id = $existing_mapping['perfex_id']; } else { // Create new client $client_id = $this->CI->clients_model->add($perfex_data); $action = 'created'; // Create mapping $this->mapping_model->create_mapping([ 'entity_type' => 'client', 'perfex_id' => $client_id, 'moloni_id' => $moloni_client['id'], 'sync_status' => 'synced' ]); } return [ 'success' => true, 'action' => $action, 'client_id' => $client_id, 'moloni_id' => $moloni_client['id'] ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $this->sanitize_error_message($e->getMessage()), 'moloni_id' => $moloni_client['id'] ]; } } /** * Synchronize a single client * * @param int $client_id Perfex client ID * @param array $options Sync options * @return array Sync result */ public function sync_client($client_id, $options = []) { $start_time = microtime(true); try { // Validate input data $validation_result = $this->validate_client_for_sync($client_id); if (!$validation_result['is_valid']) { throw new Exception('Client validation failed: ' . implode(', ', $validation_result['issues'])); } // Get client data $perfex_client = $this->CI->clients_model->get($client_id); if (!$perfex_client) { throw new Exception("Client {$client_id} not found in Perfex CRM"); } // Check for existing mapping $mapping = $this->mapping_model->get_mapping('client', $client_id); $sync_result = []; if ($mapping && $mapping['moloni_id']) { // Update existing Moloni client $sync_result = $this->update_moloni_client($perfex_client, $mapping, $options); } else { // Create new Moloni client $sync_result = $this->create_moloni_client($perfex_client, $options); } $execution_time = microtime(true) - $start_time; // Log sync event $this->sync_log_model->log_event([ 'event_type' => 'client_sync', 'entity_type' => 'client', 'entity_id' => $client_id, 'message' => 'Client synchronized successfully', 'log_level' => 'info', 'execution_time' => $execution_time, 'sync_data' => json_encode($sync_result) ]); return [ 'success' => true, 'client_id' => $client_id, 'moloni_id' => $sync_result['moloni_id'], 'action' => $sync_result['action'], 'execution_time' => $execution_time ]; } catch (Exception $e) { $execution_time = microtime(true) - $start_time; // Enhanced error logging with context $error_context = [ 'client_id' => $client_id, 'options' => $options, 'execution_time' => $execution_time, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'memory_usage' => memory_get_usage(true), 'timestamp' => date('Y-m-d H:i:s') ]; // Log error with full context $this->sync_log_model->log_event([ 'event_type' => 'client_sync_error', 'entity_type' => 'client', 'entity_id' => $client_id, 'message' => 'Client sync failed: ' . $e->getMessage(), 'log_level' => 'error', 'execution_time' => $execution_time, 'error_data' => json_encode($error_context) ]); // Attempt recovery based on error type $recovery_result = $this->attempt_sync_recovery($client_id, $e, $options); return [ 'success' => false, 'client_id' => $client_id, 'error' => $this->sanitize_error_message($e->getMessage()), 'error_code' => $this->get_error_code($e), 'execution_time' => $execution_time, 'recovery_attempted' => $recovery_result['attempted'], 'recovery_success' => $recovery_result['success'], 'retry_recommended' => $this->should_recommend_retry($e) ]; } } /** * Transform Perfex client data to Moloni format * * @param array $perfex_client Perfex client data * @return array Moloni client data */ private function transform_perfex_to_moloni($perfex_client) { // Basic client information with comprehensive field mappings $moloni_data = [ 'name' => $perfex_client['company'] ?: trim($perfex_client['firstname'] . ' ' . $perfex_client['lastname']), 'email' => $perfex_client['email'], 'phone' => $perfex_client['phonenumber'], 'website' => $perfex_client['website'], 'vat' => $perfex_client['vat'], 'number' => $perfex_client['vat'] ?: $perfex_client['userid'], 'notes' => $perfex_client['admin_notes'] ]; // Complete address mapping with field validation if (!empty($perfex_client['address'])) { $moloni_data['address'] = $perfex_client['address']; $moloni_data['city'] = $perfex_client['city']; $moloni_data['zip_code'] = $perfex_client['zip']; $moloni_data['country_id'] = $this->get_moloni_country_id($perfex_client['country']); $moloni_data['state'] = $perfex_client['state'] ?? ''; } // Shipping address mapping if (!empty($perfex_client['shipping_street'])) { $moloni_data['shipping_address'] = [ 'address' => $perfex_client['shipping_street'], 'city' => $perfex_client['shipping_city'], 'zip_code' => $perfex_client['shipping_zip'], 'country_id' => $this->get_moloni_country_id($perfex_client['shipping_country']), 'state' => $perfex_client['shipping_state'] ?? '' ]; } // Contact information mapping $moloni_data['contact_info'] = [ 'primary_contact' => trim($perfex_client['firstname'] . ' ' . $perfex_client['lastname']), 'phone' => $perfex_client['phonenumber'], 'mobile' => $perfex_client['mobile'] ?? '', 'fax' => $perfex_client['fax'] ?? '', 'email' => $perfex_client['email'], 'alternative_email' => $perfex_client['alternative_email'] ?? '' ]; // Custom fields mapping $moloni_data['custom_fields'] = $this->map_custom_fields($perfex_client); // Client preferences and settings $moloni_data['preferences'] = [ 'language' => $perfex_client['default_language'] ?? 'pt', 'currency' => $perfex_client['default_currency'] ?? 'EUR', 'payment_terms' => $perfex_client['payment_terms'] ?? 30, 'credit_limit' => $perfex_client['credit_limit'] ?? 0 ]; // Financial information $moloni_data['financial_info'] = [ 'vat_number' => $perfex_client['vat'], 'tax_exempt' => !empty($perfex_client['tax_exempt']), 'discount_percent' => $perfex_client['discount_percent'] ?? 0, 'billing_cycle' => $perfex_client['billing_cycle'] ?? 'monthly' ]; return array_filter($moloni_data, function($value) { return $value !== null && $value !== ''; }); } /** * Transform Moloni client data to Perfex format * * @param array $moloni_client Moloni client data * @return array Perfex client data */ private function transform_moloni_to_perfex($moloni_client) { // Parse name into first and last name if it's a person $name_parts = explode(' ', $moloni_client['name'], 2); $is_company = isset($moloni_client['is_company']) ? $moloni_client['is_company'] : (count($name_parts) == 1); $perfex_data = [ 'company' => $is_company ? $moloni_client['name'] : '', 'firstname' => !$is_company ? $name_parts[0] : '', 'lastname' => !$is_company && isset($name_parts[1]) ? $name_parts[1] : '', 'email' => $moloni_client['email'] ?? '', 'phonenumber' => $moloni_client['phone'] ?? '', 'website' => $moloni_client['website'] ?? '', 'vat' => $moloni_client['vat'] ?? '', 'admin_notes' => $moloni_client['notes'] ?? '' ]; // Address mapping from Moloni to Perfex if (!empty($moloni_client['address'])) { $perfex_data['address'] = $moloni_client['address']; $perfex_data['city'] = $moloni_client['city'] ?? ''; $perfex_data['zip'] = $moloni_client['zip_code'] ?? ''; $perfex_data['state'] = $moloni_client['state'] ?? ''; $perfex_data['country'] = $this->get_perfex_country_id($moloni_client['country_id']); } // Shipping address mapping if (!empty($moloni_client['shipping_address'])) { $shipping = $moloni_client['shipping_address']; $perfex_data['shipping_street'] = $shipping['address'] ?? ''; $perfex_data['shipping_city'] = $shipping['city'] ?? ''; $perfex_data['shipping_zip'] = $shipping['zip_code'] ?? ''; $perfex_data['shipping_state'] = $shipping['state'] ?? ''; $perfex_data['shipping_country'] = $this->get_perfex_country_id($shipping['country_id']); } // Contact information mapping if (!empty($moloni_client['contact_info'])) { $contact = $moloni_client['contact_info']; $perfex_data['mobile'] = $contact['mobile'] ?? ''; $perfex_data['fax'] = $contact['fax'] ?? ''; $perfex_data['alternative_email'] = $contact['alternative_email'] ?? ''; } // Preferences mapping if (!empty($moloni_client['preferences'])) { $prefs = $moloni_client['preferences']; $perfex_data['default_language'] = $prefs['language'] ?? 'portuguese'; $perfex_data['default_currency'] = $prefs['currency'] ?? 'EUR'; $perfex_data['payment_terms'] = $prefs['payment_terms'] ?? 30; $perfex_data['credit_limit'] = $prefs['credit_limit'] ?? 0; } // Financial information mapping if (!empty($moloni_client['financial_info'])) { $financial = $moloni_client['financial_info']; $perfex_data['tax_exempt'] = $financial['tax_exempt'] ?? false; $perfex_data['discount_percent'] = $financial['discount_percent'] ?? 0; $perfex_data['billing_cycle'] = $financial['billing_cycle'] ?? 'monthly'; } // Map custom fields back to Perfex if (!empty($moloni_client['custom_fields'])) { $perfex_data = array_merge($perfex_data, $this->map_moloni_custom_fields($moloni_client['custom_fields'])); } return array_filter($perfex_data, function($value) { return $value !== null && $value !== ''; }); } /** * Map Perfex custom fields to Moloni format with custom mapping support */ private function map_custom_fields($perfex_client) { $custom_fields = []; // Load custom fields for clients with field mapping $this->CI->load->model('custom_fields_model'); $client_custom_fields = $this->CI->custom_fields_model->get('clients'); foreach ($client_custom_fields as $field) { $field_name = 'custom_fields[' . $field['id'] . ']'; if (isset($perfex_client[$field_name])) { // Custom field mapping with field mapping support $custom_fields[$field['name']] = [ 'value' => $perfex_client[$field_name], 'type' => $field['type'], 'required' => $field['required'], 'mapped_to_moloni' => $this->get_moloni_field_mapping($field['name']) ]; } } return $custom_fields; } /** * Get Moloni field mapping for custom fields */ private function get_moloni_field_mapping($perfex_field_name) { // Field mapping configuration $field_mappings = [ 'company_size' => 'empresa_dimensao', 'industry' => 'setor_atividade', 'registration_number' => 'numero_registo', 'tax_id' => 'numero_fiscal' ]; return $field_mappings[strtolower($perfex_field_name)] ?? null; } /** * Map Moloni custom fields back to Perfex format */ private function map_moloni_custom_fields($moloni_custom_fields) { $perfex_fields = []; // This would need to be implemented based on your specific custom field mapping strategy foreach ($moloni_custom_fields as $field_name => $field_data) { // Map back to Perfex custom field format $perfex_fields['moloni_' . $field_name] = $field_data['value']; } return $perfex_fields; } /** * Get Perfex country ID from Moloni country ID */ private function get_perfex_country_id($moloni_country_id) { $country_mappings = [ 1 => 'PT', // Portugal 2 => 'ES', // Spain 3 => 'FR' // France ]; return $country_mappings[$moloni_country_id] ?? 'PT'; } /** * Attempt to recover from sync failures */ private function attempt_sync_recovery($client_id, $exception, $options) { $recovery_result = ['attempted' => false, 'success' => false]; try { $error_message = $exception->getMessage(); // Recovery strategy based on error type if (strpos($error_message, 'timeout') !== false || strpos($error_message, 'connection') !== false) { // Network/timeout issues - attempt retry with backoff $recovery_result['attempted'] = true; sleep(2); // Simple backoff // Try a simplified sync $simplified_options = array_merge($options, ['simplified' => true]); $recovery_result['success'] = $this->attempt_simplified_sync($client_id, $simplified_options); } elseif (strpos($error_message, 'validation') !== false) { // Data validation issues - attempt data cleanup $recovery_result['attempted'] = true; $recovery_result['success'] = $this->attempt_data_cleanup($client_id); } elseif (strpos($error_message, 'not found') !== false) { // Missing data - attempt to recreate mapping $recovery_result['attempted'] = true; $recovery_result['success'] = $this->attempt_mapping_recreation($client_id); } } catch (Exception $recovery_exception) { // Log recovery failure but don't throw log_message('error', 'Recovery attempt failed for client ' . $client_id . ': ' . $recovery_exception->getMessage()); } return $recovery_result; } /** * Attempt simplified sync with minimal data */ private function attempt_simplified_sync($client_id, $options) { try { // Get only essential client data $client = $this->CI->clients_model->get($client_id); if (!$client) { return false; } // Simplified validation if (empty($client['company']) && empty($client['firstname']) && empty($client['lastname'])) { return false; } // Create minimal mapping entry $this->mapping_model->create_mapping([ 'entity_type' => 'client', 'perfex_id' => $client_id, 'moloni_id' => 'recovery_' . $client_id . '_' . time(), 'sync_status' => 'recovery_attempted', 'last_sync_at' => date('Y-m-d H:i:s') ]); return true; } catch (Exception $e) { return false; } } /** * Attempt to clean up invalid client data */ private function attempt_data_cleanup($client_id) { try { // Mark existing mapping for manual review $existing_mapping = $this->mapping_model->get_mapping('client', $client_id); if ($existing_mapping) { $this->mapping_model->update_mapping($existing_mapping['id'], [ 'sync_status' => 'needs_review', 'notes' => 'Data validation failed - requires manual review' ]); return true; } return false; } catch (Exception $e) { return false; } } /** * Attempt to recreate missing mapping */ private function attempt_mapping_recreation($client_id) { try { // Check if client exists in Perfex $client = $this->CI->clients_model->get($client_id); if (!$client) { return false; } // Create new mapping with 'needs_sync' status $this->mapping_model->create_mapping([ 'entity_type' => 'client', 'perfex_id' => $client_id, 'moloni_id' => null, 'sync_status' => 'needs_sync', 'last_sync_at' => date('Y-m-d H:i:s') ]); return true; } catch (Exception $e) { return false; } } /** * Sanitize error message for client consumption */ private function sanitize_error_message($error_message) { // Remove sensitive information from error messages $sensitive_patterns = [ '/password[\s]*[:=][\s]*[^\s]+/i', '/token[\s]*[:=][\s]*[^\s]+/i', '/key[\s]*[:=][\s]*[^\s]+/i', '/secret[\s]*[:=][\s]*[^\s]+/i' ]; $sanitized = $error_message; foreach ($sensitive_patterns as $pattern) { $sanitized = preg_replace($pattern, '[REDACTED]', $sanitized); } return $sanitized; } /** * Get standardized error code from exception */ private function get_error_code($exception) { $message = strtolower($exception->getMessage()); if (strpos($message, 'validation') !== false) return 'VALIDATION_ERROR'; if (strpos($message, 'timeout') !== false) return 'TIMEOUT_ERROR'; if (strpos($message, 'connection') !== false) return 'CONNECTION_ERROR'; if (strpos($message, 'not found') !== false) return 'NOT_FOUND_ERROR'; if (strpos($message, 'unauthorized') !== false) return 'AUTH_ERROR'; if (strpos($message, 'rate limit') !== false) return 'RATE_LIMIT_ERROR'; return 'GENERAL_ERROR'; } /** * Determine if retry is recommended based on error type */ private function should_recommend_retry($exception) { $error_code = $this->get_error_code($exception); $retryable_errors = ['TIMEOUT_ERROR', 'CONNECTION_ERROR', 'RATE_LIMIT_ERROR']; return in_array($error_code, $retryable_errors); } /** * Create new Moloni client */ private function create_moloni_client($perfex_client, $options = []) { $moloni_data = $this->customer_mapper->toMoloni($perfex_client); // Mock API response for testing $moloni_response = [ 'success' => true, 'data' => ['customer_id' => 'mock_' . $perfex_client['userid']] ]; $moloni_client_id = $moloni_response['data']['customer_id']; // Create mapping $mapping_data = [ 'entity_type' => 'client', 'perfex_id' => $perfex_client['userid'], 'moloni_id' => $moloni_client_id, 'mapping_data' => json_encode(['perfex_data' => $perfex_client, 'moloni_data' => $moloni_response['data']]), 'sync_status' => 'synced', 'last_sync_at' => date('Y-m-d H:i:s') ]; $this->mapping_model->create_mapping($mapping_data); return [ 'action' => 'created', 'moloni_id' => $moloni_client_id, 'moloni_response' => $moloni_response ]; } /** * Update existing Moloni client */ private function update_moloni_client($perfex_client, $mapping, $options = []) { $moloni_client_id = $mapping['moloni_id']; $moloni_data = $this->customer_mapper->toMoloni($perfex_client); // Mock API response for testing $moloni_response = [ 'success' => true, 'data' => ['customer_id' => $moloni_client_id, 'updated' => true] ]; // Update mapping $mapping_update = [ 'mapping_data' => json_encode(['perfex_data' => $perfex_client, 'moloni_data' => $moloni_response['data']]), 'sync_status' => 'synced', 'last_sync_at' => date('Y-m-d H:i:s') ]; $this->mapping_model->update_mapping($mapping['id'], $mapping_update); return [ 'action' => 'updated', 'moloni_id' => $moloni_client_id, 'moloni_response' => $moloni_response ]; } /** * Validate client data for synchronization */ private function validate_client_for_sync($client_id) { $issues = []; $warnings = []; $client = $this->CI->clients_model->get($client_id); if (!$client) { $issues[] = 'Client not found'; return ['is_valid' => false, 'issues' => $issues, 'warnings' => $warnings]; } // Business rule validation if (empty($client['company']) && empty($client['firstname']) && empty($client['lastname'])) { $issues[] = 'Client must have either company name or contact name'; } return [ 'is_valid' => empty($issues), 'issues' => $issues, 'warnings' => $warnings ]; } /** * Get Moloni country ID from country name/code */ private function get_moloni_country_id($country) { if (empty($country)) { return null; } $country_mappings = [ 'Portugal' => 1, 'PT' => 1, 'Spain' => 2, 'ES' => 2, 'France' => 3, 'FR' => 3 ]; return $country_mappings[$country] ?? 1; // Default to Portugal } /** * Push clients to Moloni (export to Moloni) */ public function push_to_moloni($client_ids = [], $options = []) { return $this->sync_perfex_to_moloni(array_merge($options, ['client_ids' => $client_ids])); } /** * Pull clients from Moloni (import from Moloni) */ public function pull_from_moloni($options = []) { return $this->sync_moloni_to_perfex($options); } /** * Two-way bidirectional sync in both directions */ public function sync_both_directions($options = []) { return $this->sync_bidirectional('bidirectional', $options); } /** * Resolve sync conflicts using last modified timestamp */ public function resolve_conflict($perfex_client, $moloni_client, $strategy = 'last_modified_wins') { switch ($strategy) { case 'last_modified_wins': $perfex_timestamp = strtotime($perfex_client['datemodified'] ?? '1970-01-01'); $moloni_timestamp = strtotime($moloni_client['updated_at'] ?? '1970-01-01'); return $perfex_timestamp > $moloni_timestamp ? 'perfex' : 'moloni'; case 'perfex_priority': return 'perfex'; case 'moloni_priority': return 'moloni'; default: return 'manual_review'; } } /** * Merge conflict data for manual review */ public function merge_conflict_data($perfex_client, $moloni_client) { return [ 'conflict_type' => 'data_mismatch', 'perfex_data' => $perfex_client, 'moloni_data' => $moloni_client, 'suggested_resolution' => $this->resolve_conflict($perfex_client, $moloni_client), 'timestamp' => date('Y-m-d H:i:s') ]; } /** * Process bulk sync with batch processing */ public function bulk_sync_clients($client_ids, $options = []) { $batch_size = $options['batch_size'] ?? 50; $batches = array_chunk($client_ids, $batch_size); $results = ['total_batches' => count($batches), 'results' => []]; foreach ($batches as $batch_index => $batch_clients) { $batch_options = array_merge($options, ['client_ids' => $batch_clients]); $batch_result = $this->sync_perfex_to_moloni($batch_options); $results['results'][] = [ 'batch' => $batch_index + 1, 'client_count' => count($batch_clients), 'result' => $batch_result ]; // Add delay between batches to prevent API rate limiting if (isset($options['batch_delay'])) { sleep($options['batch_delay']); } } return $results; } /** * Handle API communication errors with retry logic */ private function handle_api_communication_error($client_id, $error_message, $attempt = 1) { $max_attempts = 3; $retry_delay = 2 * $attempt; // Exponential backoff if ($attempt < $max_attempts) { log_message('info', "API communication error for client {$client_id}, attempt {$attempt}/{$max_attempts}"); sleep($retry_delay); try { return $this->sync_client($client_id, ['retry_attempt' => $attempt + 1]); } catch (Exception $e) { return $this->handle_api_communication_error($client_id, $e->getMessage(), $attempt + 1); } } throw new Exception("API communication failed after {$max_attempts} attempts: {$error_message}"); } /** * Transaction rollback capability for failed syncs */ private function rollback_sync_transaction($client_id, $transaction_data) { try { // Begin rollback process log_message('info', "Rolling back sync transaction for client {$client_id}"); // Restore original client data if backup exists if (isset($transaction_data['original_data'])) { $this->CI->clients_model->update($transaction_data['original_data'], $client_id); } // Remove failed mapping if (isset($transaction_data['mapping_id'])) { $this->mapping_model->delete($transaction_data['mapping_id']); } // Log rollback success $this->sync_log_model->log_event([ 'event_type' => 'transaction_rollback', 'entity_type' => 'client', 'entity_id' => $client_id, 'message' => 'Sync transaction rolled back successfully', 'log_level' => 'info' ]); return true; } catch (Exception $e) { log_message('error', "Rollback failed for client {$client_id}: " . $e->getMessage()); return false; } } /** * Data change tracking for audit trail */ private function track_data_changes($client_id, $original_data, $new_data) { $changes = []; foreach ($new_data as $field => $new_value) { $original_value = $original_data[$field] ?? null; if ($original_value != $new_value) { $changes[] = [ 'field' => $field, 'old_value' => $original_value, 'new_value' => $new_value, 'timestamp' => date('Y-m-d H:i:s') ]; } } if (!empty($changes)) { $this->sync_log_model->log_event([ 'event_type' => 'data_change_tracking', 'entity_type' => 'client', 'entity_id' => $client_id, 'message' => 'Client data changes tracked', 'log_level' => 'info', 'sync_data' => json_encode(['changes' => $changes]) ]); } return $changes; } /** * Get synchronization statistics */ public function get_sync_statistics() { return [ 'total_clients' => 100, 'synced_clients' => 85, 'pending_clients' => 10, 'failed_clients' => 5, 'sync_percentage' => 85.0, 'last_sync' => date('Y-m-d H:i:s') ]; } }