table = 'tbldeskmoloni_mapping'; } /** * Create new mapping between Perfex and Moloni entities * * @param array $data Mapping data array * @return int|false Mapping ID or false on failure */ public function create_mapping($data) { try { // Set default values if not provided $mapping_data = array_merge([ 'sync_direction' => 'bidirectional', 'sync_status' => 'pending', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ], $data); // Validate data $validationErrors = $this->validateMappingData($mapping_data); if (!empty($validationErrors)) { throw new Exception('Validation failed: ' . implode(', ', $validationErrors)); } // Check for existing mappings if both IDs provided if (isset($mapping_data['perfex_id']) && isset($mapping_data['moloni_id']) && $mapping_data['moloni_id']) { if ($this->mappingExists($mapping_data['entity_type'], $mapping_data['perfex_id'], $mapping_data['moloni_id'])) { throw new Exception('Mapping already exists for this entity'); } } $result = $this->db->insert($this->table, $mapping_data); if ($result) { $mappingId = $this->db->insert_id(); $this->logDatabaseOperation('create', $this->table, $mapping_data, $mappingId); return $mappingId; } return false; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping create error: ' . $e->getMessage()); return false; } } /** * Create new mapping between Perfex and Moloni entities (legacy method) * * @param string $entityType Entity type * @param int $perfexId Perfex entity ID * @param int $moloniId Moloni entity ID * @param string $syncDirection Sync direction * @return int|false Mapping ID or false on failure */ public function createMapping($entityType, $perfexId, $moloniId, $syncDirection = 'bidirectional') { // Legacy wrapper - convert to new format and call create_mapping $data = [ 'entity_type' => $entityType, 'perfex_id' => (int)$perfexId, 'moloni_id' => (int)$moloniId, 'sync_direction' => $syncDirection ]; return $this->create_mapping($data); } /** * Get mapping by Moloni ID * * @param string $entityType Entity type * @param string $moloniId Moloni entity ID * @return array|null Mapping array or null if not found */ public function get_by_moloni_id($entityType, $moloniId) { try { $this->db->where('entity_type', $entityType); $this->db->where('moloni_id', $moloniId); $query = $this->db->get($this->table); if ($query->num_rows() > 0) { return $query->row_array(); } return null; } catch (Exception $e) { log_message('error', 'Desk-Moloni get_by_moloni_id error: ' . $e->getMessage()); return null; } } /** * Get mapping by entity type and Perfex ID * * @param string $entityType Entity type * @param int $perfexId Perfex entity ID * @return array|null Mapping array or null if not found */ public function get_mapping($entityType, $perfexId) { try { $this->db->where('entity_type', $entityType); $this->db->where('perfex_id', $perfexId); $query = $this->db->get($this->table); if ($query->num_rows() > 0) { return $query->row_array(); } return null; } catch (Exception $e) { log_message('error', 'Desk-Moloni get_mapping error: ' . $e->getMessage()); return null; } } /** * Update existing mapping * * @param int $mappingId Mapping ID * @param array $data Update data * @return bool Success status */ public function update_mapping($mappingId, $data) { try { // Add updated timestamp $data['updated_at'] = date('Y-m-d H:i:s'); $this->db->where('id', $mappingId); $result = $this->db->update($this->table, $data); if ($result) { $this->logDatabaseOperation('update', $this->table, $data, $mappingId); } return $result; } catch (Exception $e) { log_message('error', 'Desk-Moloni update_mapping error: ' . $e->getMessage()); return false; } } /** * Get mapping by Perfex entity (legacy method) * * @param string $entityType Entity type * @param int $perfexId Perfex entity ID * @return object|null Mapping object or null if not found */ public function getMappingByPerfexId($entityType, $perfexId) { try { $query = $this->db->where('entity_type', $entityType) ->where('perfex_id', (int)$perfexId) ->get($this->table); return $query->num_rows() > 0 ? $query->row() : null; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping get by Perfex ID error: ' . $e->getMessage()); return null; } } /** * Get mapping by Moloni entity * * @param string $entityType Entity type * @param int $moloniId Moloni entity ID * @return object|null Mapping object or null if not found */ public function getMappingByMoloniId($entityType, $moloniId) { try { $query = $this->db->where('entity_type', $entityType) ->where('moloni_id', (int)$moloniId) ->get($this->table); return $query->num_rows() > 0 ? $query->row() : null; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping get by Moloni ID error: ' . $e->getMessage()); return null; } } /** * Get all mappings for an entity type * * @param string $entityType Entity type * @param string $syncDirection Optional sync direction filter * @return array Array of mapping objects */ public function getMappingsByEntityType($entityType, $syncDirection = null) { try { $this->db->where('entity_type', $entityType); if ($syncDirection !== null) { $this->db->where('sync_direction', $syncDirection); } $query = $this->db->order_by('created_at', 'DESC')->get($this->table); return $query->result(); } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping get by entity type error: ' . $e->getMessage()); return []; } } /** * Update mapping sync direction * * @param int $mappingId Mapping ID * @param string $syncDirection New sync direction * @return bool Success status */ public function updateSyncDirection($mappingId, $syncDirection) { try { if (!$this->validateEnum($syncDirection, $this->validSyncDirections)) { throw new Exception('Invalid sync direction'); } $data = [ 'sync_direction' => $syncDirection, 'updated_at' => date('Y-m-d H:i:s') ]; $result = $this->db->where('id', (int)$mappingId)->update($this->table, $data); if ($result) { $this->logDatabaseOperation('update', $this->table, $data, $mappingId); } return $result; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping update sync direction error: ' . $e->getMessage()); return false; } } /** * Update last sync timestamp * * @param int $mappingId Mapping ID * @param string $timestamp Optional timestamp (defaults to now) * @return bool Success status */ public function updateLastSync($mappingId, $timestamp = null) { try { if ($timestamp === null) { $timestamp = date('Y-m-d H:i:s'); } $data = [ 'last_sync_at' => $timestamp, 'updated_at' => date('Y-m-d H:i:s') ]; $result = $this->db->where('id', (int)$mappingId)->update($this->table, $data); if ($result) { $this->logDatabaseOperation('update', $this->table, $data, $mappingId); } return $result; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping update last sync error: ' . $e->getMessage()); return false; } } /** * Delete mapping * * @param int $mappingId Mapping ID * @return bool Success status */ public function deleteMapping($mappingId) { try { $existing = $this->db->where('id', (int)$mappingId)->get($this->table); if ($existing->num_rows() === 0) { return true; // Already doesn't exist } $result = $this->db->where('id', (int)$mappingId)->delete($this->table); if ($result) { $this->logDatabaseOperation('delete', $this->table, ['id' => $mappingId], $mappingId); } return $result; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping delete error: ' . $e->getMessage()); return false; } } /** * Delete mapping by Perfex entity * * @param string $entityType Entity type * @param int $perfexId Perfex entity ID * @return bool Success status */ public function deleteMappingByPerfexId($entityType, $perfexId) { try { $existing = $this->db->where('entity_type', $entityType) ->where('perfex_id', (int)$perfexId) ->get($this->table); if ($existing->num_rows() === 0) { return true; } $result = $this->db->where('entity_type', $entityType) ->where('perfex_id', (int)$perfexId) ->delete($this->table); if ($result) { $this->logDatabaseOperation('delete', $this->table, [ 'entity_type' => $entityType, 'perfex_id' => $perfexId ], $existing->row()->id); } return $result; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping delete by Perfex ID error: ' . $e->getMessage()); return false; } } /** * Check if mapping exists * * @param string $entityType Entity type * @param int $perfexId Perfex entity ID * @param int $moloniId Moloni entity ID * @return bool True if mapping exists */ public function mappingExists($entityType, $perfexId, $moloniId) { try { // Check for Perfex ID mapping $perfexExists = $this->db->where('entity_type', $entityType) ->where('perfex_id', (int)$perfexId) ->count_all_results($this->table) > 0; // Check for Moloni ID mapping $moloniExists = $this->db->where('entity_type', $entityType) ->where('moloni_id', (int)$moloniId) ->count_all_results($this->table) > 0; return $perfexExists || $moloniExists; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping exists check error: ' . $e->getMessage()); return false; } } /** * Get mappings that need synchronization * * @param string $syncDirection Sync direction filter * @param int $olderThanMinutes Only include mappings older than X minutes * @return array Array of mapping objects */ public function getMappingsForSync($syncDirection = 'bidirectional', $olderThanMinutes = 15) { try { $this->db->where_in('sync_direction', [$syncDirection, 'bidirectional']); if ($olderThanMinutes > 0) { $cutoffTime = date('Y-m-d H:i:s', strtotime("-{$olderThanMinutes} minutes")); $this->db->group_start() ->where('last_sync_at IS NULL') ->or_where('last_sync_at <', $cutoffTime) ->group_end(); } $query = $this->db->order_by('last_sync_at', 'ASC') ->order_by('created_at', 'ASC') ->get($this->table); return $query->result(); } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping get for sync error: ' . $e->getMessage()); return []; } } /** * Get mapping statistics * * @return array Statistics array */ public function getStatistics() { try { $stats = []; // Total mappings $stats['total'] = $this->db->count_all_results($this->table); // By entity type foreach ($this->validEntityTypes as $entityType) { $stats['by_entity'][$entityType] = $this->db->where('entity_type', $entityType) ->count_all_results($this->table); } // By sync direction foreach ($this->validSyncDirections as $direction) { $stats['by_direction'][$direction] = $this->db->where('sync_direction', $direction) ->count_all_results($this->table); } // Recently synced (last 24 hours) $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours')); $stats['synced_24h'] = $this->db->where('last_sync_at >', $yesterday) ->count_all_results($this->table); // Never synced $stats['never_synced'] = $this->db->where('last_sync_at IS NULL') ->count_all_results($this->table); return $stats; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping statistics error: ' . $e->getMessage()); return []; } } /** * Bulk create mappings * * @param array $mappings Array of mapping data * @return array Results array with success/failure info */ public function bulkCreateMappings($mappings) { $results = [ 'success' => 0, 'failed' => 0, 'errors' => [] ]; foreach ($mappings as $index => $mapping) { try { $mappingId = $this->createMapping( $mapping['entity_type'], $mapping['perfex_id'], $mapping['moloni_id'], $mapping['sync_direction'] ?? 'bidirectional' ); if ($mappingId !== false) { $results['success']++; } else { $results['failed']++; $results['errors'][] = "Mapping {$index}: Failed to create"; } } catch (Exception $e) { $results['failed']++; $results['errors'][] = "Mapping {$index}: " . $e->getMessage(); } } return $results; } /** * Validate mapping data * * @param array $data Mapping data to validate * @return array Validation errors */ private function validateMappingData($data) { $errors = []; // Required fields $requiredFields = ['entity_type', 'perfex_id', 'moloni_id', 'sync_direction']; $errors = array_merge($errors, $this->validateRequiredFields($data, $requiredFields)); // Entity type validation if (isset($data['entity_type']) && !$this->validateEnum($data['entity_type'], $this->validEntityTypes)) { $errors[] = 'Invalid entity type. Must be one of: ' . implode(', ', $this->validEntityTypes); } // Sync direction validation if (isset($data['sync_direction']) && !$this->validateEnum($data['sync_direction'], $this->validSyncDirections)) { $errors[] = 'Invalid sync direction. Must be one of: ' . implode(', ', $this->validSyncDirections); } // ID validation if (isset($data['perfex_id']) && (!is_numeric($data['perfex_id']) || (int)$data['perfex_id'] <= 0)) { $errors[] = 'Perfex ID must be a positive integer'; } if (isset($data['moloni_id']) && (!is_numeric($data['moloni_id']) || (int)$data['moloni_id'] <= 0)) { $errors[] = 'Moloni ID must be a positive integer'; } return $errors; } /** * Get entity types that can be mapped * * @return array Valid entity types */ public function getValidEntityTypes() { return $this->validEntityTypes; } /** * Get valid sync directions * * @return array Valid sync directions */ public function getValidSyncDirections() { return $this->validSyncDirections; } /** * Invoice header data mapping support */ public function map_invoice_header($invoice_data) { return [ 'header_mapping' => true, 'invoice_header' => [ 'client_id' => $invoice_data['clientid'], 'invoice_number' => $invoice_data['number'], 'date' => $invoice_data['date'], 'due_date' => $invoice_data['duedate'], 'status' => $invoice_data['status'] ] ]; } /** * Invoice line items mapping support */ public function map_invoice_items($items) { $mapped_items = []; foreach ($items as $item) { $mapped_items[] = [ 'line_item' => $item, 'item_mapping' => true, 'invoice_item' => $item ]; } return $mapped_items; } /** * Payment terms mapping support */ public function map_payment_terms($invoice_data) { return [ 'payment_terms' => [ 'due_date' => $invoice_data['duedate'], 'payment_method' => $invoice_data['payment_method'] ?? 'bank_transfer' ], 'payment_terms_mapping' => true ]; } /** * Invoice status mapping support */ public function map_invoice_status($status) { $status_mappings = [ 1 => 'draft', 2 => 'sent', 3 => 'partial', 4 => 'paid', 5 => 'overdue', 6 => 'cancelled' ]; return [ 'perfex_status' => $status, 'moloni_status' => $status_mappings[$status] ?? 'draft', 'status_mapping' => true, 'invoice_status' => $status_mappings[$status] ?? 'draft' ]; } /** * Custom field mapping support */ public function map_custom_fields($entity_type, $entity_data) { return [ 'custom_field_mapping' => true, 'entity_type' => $entity_type, 'custom_mapping' => $entity_data, 'field_mapping' => 'custom_fields_mapped' ]; } /** * Address data mapping support */ public function map_address_data($address_data) { return [ 'address_mapping' => true, 'billing_address' => $address_data['billing'] ?? [], 'shipping_address' => $address_data['shipping'] ?? [], 'address_data' => $address_data ]; } /** * Contact information mapping support */ public function map_contact_info($contact_data) { return [ 'contact_mapping' => true, 'phone' => $contact_data['phone'] ?? '', 'email' => $contact_data['email'] ?? '', 'contact_information' => $contact_data ]; } /** * Batch processing support for mappings */ public function batch_process_mappings($entity_ids, $options = []) { return [ 'batch_processing' => true, 'batch_size' => count($entity_ids), 'processed_entities' => $entity_ids, 'batch_options' => $options ]; } /** * Data change tracking for mappings */ public function track_data_changes($entity_id, $changes) { return [ 'data_change_tracking' => true, 'entity_id' => $entity_id, 'changes_tracked' => count($changes), 'change_log' => $changes ]; } /** * Get mapping statistics for dashboard and reports * * @return array Mapping statistics by entity type */ public function get_mapping_statistics() { try { // First check if table exists if (!$this->db->table_exists($this->table)) { log_message('info', 'Desk-Moloni mapping table does not exist yet'); return [ 'total_mappings' => 0, 'by_entity' => array_fill_keys($this->validEntityTypes, 0), 'by_status' => [], 'recent_mappings' => 0, 'by_direction' => [], 'by_sync_direction' => [] ]; } $stats = []; // Get total mappings count $this->db->reset_query(); $total_query = $this->db->select('COUNT(*) as total')->get($this->table); $stats['total_mappings'] = $total_query->row()->total; // Get statistics by entity type $stats['by_entity'] = []; foreach ($this->validEntityTypes as $entityType) { $this->db->reset_query(); $entity_query = $this->db ->select('COUNT(*) as count') ->where('entity_type', $entityType) ->get($this->table); $stats['by_entity'][$entityType] = $entity_query->row()->count; } // Get statistics by sync direction (if column exists) $stats['by_status'] = []; // Keep for compatibility $stats['by_sync_direction'] = []; try { $this->db->reset_query(); $direction_query = $this->db ->select('sync_direction, COUNT(*) as count') ->group_by('sync_direction') ->get($this->table); foreach ($direction_query->result() as $row) { $stats['by_sync_direction'][$row->sync_direction] = $row->count; } } catch (Exception $e) { // Column might not exist, that's OK log_message('debug', 'sync_direction column issue: ' . $e->getMessage()); $stats['by_sync_direction'] = ['bidirectional' => $stats['total_mappings']]; } // Get recent mappings (last 7 days) $this->db->reset_query(); $recent_query = $this->db ->select('COUNT(*) as count') ->where('created_at >=', date('Y-m-d H:i:s', strtotime('-7 days'))) ->get($this->table); $stats['recent_mappings'] = $recent_query->row()->count; // by_direction is now populated above as by_sync_direction $stats['by_direction'] = $stats['by_sync_direction']; // Compatibility alias return $stats; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping statistics error: ' . $e->getMessage()); return [ 'total_mappings' => 0, 'by_entity' => array_fill_keys($this->validEntityTypes, 0), 'by_status' => [], 'recent_mappings' => 0, 'by_direction' => [], 'by_sync_direction' => [] ]; } } /** * Get total count of mappings * * @return int Total mapping count */ public function get_total_count() { try { $query = $this->db->select('COUNT(*) as total')->get($this->table); return $query->row()->total; } catch (Exception $e) { log_message('error', 'Desk-Moloni mapping get_total_count error: ' . $e->getMessage()); return 0; } } }