/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ CI = &get_instance(); $this->CI->load->model('desk_moloni_model'); $this->model = $this->CI->desk_moloni_model; log_activity('EntityMappingService initialized'); } /** * Create entity mapping * * @param string $entity_type * @param int $perfex_id * @param int $moloni_id * @param string $sync_direction * @param array $metadata * @return int|false */ public function create_mapping($entity_type, $perfex_id, $moloni_id, $sync_direction = self::DIRECTION_BIDIRECTIONAL, $metadata = []) { if (!$this->is_valid_entity_type($entity_type)) { throw new \InvalidArgumentException("Invalid entity type: {$entity_type}"); } // Check for existing mapping $existing = $this->get_mapping($entity_type, $perfex_id, $moloni_id); if ($existing) { throw new \Exception("Mapping already exists with ID: {$existing->id}"); } $mapping_data = [ 'entity_type' => $entity_type, 'perfex_id' => $perfex_id, 'moloni_id' => $moloni_id, 'sync_direction' => $sync_direction, 'sync_status' => self::STATUS_PENDING, 'metadata' => json_encode($metadata), 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; $mapping_id = $this->model->create_entity_mapping($mapping_data); if ($mapping_id) { log_activity("Created {$entity_type} mapping: Perfex #{$perfex_id} <-> Moloni #{$moloni_id}"); } return $mapping_id; } /** * Update entity mapping * * @param int $mapping_id * @param array $data * @return bool */ public function update_mapping($mapping_id, $data) { $data['updated_at'] = date('Y-m-d H:i:s'); $result = $this->model->update_entity_mapping($mapping_id, $data); if ($result) { log_activity("Updated entity mapping #{$mapping_id}"); } return $result; } /** * Get entity mapping by IDs * * @param string $entity_type * @param int $perfex_id * @param int $moloni_id * @return object|null */ public function get_mapping($entity_type, $perfex_id = null, $moloni_id = null) { if (!$perfex_id && !$moloni_id) { throw new \InvalidArgumentException("Either perfex_id or moloni_id must be provided"); } return $this->model->get_entity_mapping($entity_type, $perfex_id, $moloni_id); } /** * Get mapping by Perfex ID * * @param string $entity_type * @param int $perfex_id * @return object|null */ public function get_mapping_by_perfex_id($entity_type, $perfex_id) { return $this->model->get_entity_mapping_by_perfex_id($entity_type, $perfex_id); } /** * Get mapping by Moloni ID * * @param string $entity_type * @param int $moloni_id * @return object|null */ public function get_mapping_by_moloni_id($entity_type, $moloni_id) { return $this->model->get_entity_mapping_by_moloni_id($entity_type, $moloni_id); } /** * Delete entity mapping * * @param int $mapping_id * @return bool */ public function delete_mapping($mapping_id) { $mapping = $this->model->get_entity_mapping_by_id($mapping_id); if (!$mapping) { return false; } $result = $this->model->delete_entity_mapping($mapping_id); if ($result) { log_activity("Deleted {$mapping->entity_type} mapping #{$mapping_id}"); } return $result; } /** * Get all mappings for entity type * * @param string $entity_type * @param array $filters * @return array */ public function get_mappings_by_type($entity_type, $filters = []) { if (!$this->is_valid_entity_type($entity_type)) { throw new \InvalidArgumentException("Invalid entity type: {$entity_type}"); } return $this->model->get_entity_mappings_by_type($entity_type, $filters); } /** * Update mapping status * * @param int $mapping_id * @param string $status * @param string $error_message * @return bool */ public function update_mapping_status($mapping_id, $status, $error_message = null) { if (!in_array($status, [self::STATUS_PENDING, self::STATUS_SYNCED, self::STATUS_ERROR, self::STATUS_CONFLICT])) { throw new \InvalidArgumentException("Invalid status: {$status}"); } $data = [ 'sync_status' => $status, 'error_message' => $error_message, 'last_sync_at' => date('Y-m-d H:i:s') ]; return $this->update_mapping($mapping_id, $data); } /** * Update sync timestamps * * @param int $mapping_id * @param string $direction * @return bool */ public function update_sync_timestamp($mapping_id, $direction) { $field = $direction === self::DIRECTION_PERFEX_TO_MOLONI ? 'last_sync_perfex' : 'last_sync_moloni'; return $this->update_mapping($mapping_id, [ $field => date('Y-m-d H:i:s'), 'sync_status' => self::STATUS_SYNCED ]); } /** * Check if entity is already mapped * * @param string $entity_type * @param int $perfex_id * @param int $moloni_id * @return bool */ public function is_mapped($entity_type, $perfex_id = null, $moloni_id = null) { return $this->get_mapping($entity_type, $perfex_id, $moloni_id) !== null; } /** * Get unmapped entities * * @param string $entity_type * @param string $source_system ('perfex' or 'moloni') * @param int $limit * @return array */ public function get_unmapped_entities($entity_type, $source_system, $limit = 100) { if (!$this->is_valid_entity_type($entity_type)) { throw new \InvalidArgumentException("Invalid entity type: {$entity_type}"); } if (!in_array($source_system, ['perfex', 'moloni'])) { throw new \InvalidArgumentException("Invalid source system: {$source_system}"); } return $this->model->get_unmapped_entities($entity_type, $source_system, $limit); } /** * Get mapping statistics * * @param string $entity_type * @return array */ public function get_mapping_statistics($entity_type = null) { return $this->model->get_mapping_statistics($entity_type); } /** * Find potential matches between systems * * @param string $entity_type * @param array $search_criteria * @param string $target_system * @return array */ public function find_potential_matches($entity_type, $search_criteria, $target_system) { if (!$this->is_valid_entity_type($entity_type)) { throw new \InvalidArgumentException("Invalid entity type: {$entity_type}"); } // This will be implemented by specific sync services // Return format: [['id' => X, 'match_score' => Y, 'match_criteria' => []], ...] return []; } /** * Resolve mapping conflicts * * @param int $mapping_id * @param string $resolution ('keep_perfex', 'keep_moloni', 'merge') * @param array $merge_data * @return bool */ public function resolve_conflict($mapping_id, $resolution, $merge_data = []) { $mapping = $this->model->get_entity_mapping_by_id($mapping_id); if (!$mapping || $mapping->sync_status !== self::STATUS_CONFLICT) { throw new \Exception("Mapping not found or not in conflict state"); } switch ($resolution) { case 'keep_perfex': return $this->update_mapping_status($mapping_id, self::STATUS_SYNCED); case 'keep_moloni': return $this->update_mapping_status($mapping_id, self::STATUS_SYNCED); case 'merge': // Store merge data for processing by sync services $metadata = json_decode($mapping->metadata, true) ?: []; $metadata['merge_data'] = $merge_data; $metadata['resolution'] = 'merge'; return $this->update_mapping($mapping_id, [ 'sync_status' => self::STATUS_PENDING, 'metadata' => json_encode($metadata) ]); default: throw new \InvalidArgumentException("Invalid resolution: {$resolution}"); } } /** * Bulk create mappings * * @param array $mappings * @return array */ public function bulk_create_mappings($mappings) { $results = [ 'total' => count($mappings), 'success' => 0, 'errors' => 0, 'details' => [] ]; foreach ($mappings as $mapping) { try { $mapping_id = $this->create_mapping( $mapping['entity_type'], $mapping['perfex_id'], $mapping['moloni_id'], $mapping['sync_direction'] ?? self::DIRECTION_BIDIRECTIONAL, $mapping['metadata'] ?? [] ); $results['success']++; $results['details'][] = [ 'mapping_id' => $mapping_id, 'success' => true ]; } catch (\Exception $e) { $results['errors']++; $results['details'][] = [ 'error' => $e->getMessage(), 'success' => false, 'data' => $mapping ]; } } return $results; } /** * Clean up old mappings * * @param string $entity_type * @param int $retention_days * @return int */ public function cleanup_old_mappings($entity_type, $retention_days = 90) { $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$retention_days} days")); $deleted = $this->model->cleanup_old_mappings($entity_type, $cutoff_date); if ($deleted > 0) { log_activity("Cleaned up {$deleted} old {$entity_type} mappings older than {$retention_days} days"); } return $deleted; } /** * Validate entity type * * @param string $entity_type * @return bool */ protected function is_valid_entity_type($entity_type) { return in_array($entity_type, [ self::ENTITY_CUSTOMER, self::ENTITY_PRODUCT, self::ENTITY_INVOICE, self::ENTITY_ESTIMATE, self::ENTITY_CREDIT_NOTE ]); } /** * Export mappings to CSV * * @param string $entity_type * @param array $filters * @return string */ public function export_mappings_csv($entity_type, $filters = []) { $mappings = $this->get_mappings_by_type($entity_type, $filters); $output = fopen('php://temp', 'r+'); // CSV Header fputcsv($output, [ 'ID', 'Entity Type', 'Perfex ID', 'Moloni ID', 'Sync Direction', 'Sync Status', 'Last Sync Perfex', 'Last Sync Moloni', 'Created At', 'Updated At' ]); foreach ($mappings as $mapping) { fputcsv($output, [ $mapping->id, $mapping->entity_type, $mapping->perfex_id, $mapping->moloni_id, $mapping->sync_direction, $mapping->sync_status, $mapping->last_sync_perfex, $mapping->last_sync_moloni, $mapping->created_at, $mapping->updated_at ]); } rewind($output); $csv_content = stream_get_contents($output); fclose($output); return $csv_content; } }