/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ table = 'tbldeskmoloni_sync_queue'; } /** * Add task to sync queue * * @param string $taskType Task type * @param string $entityType Entity type * @param int $entityId Entity ID * @param array $payload Task payload data * @param int $priority Task priority (1=highest, 9=lowest) * @param string $scheduledAt When to schedule the task (defaults to now) * @return int|false Task ID or false on failure */ public function addTask($taskType, $entityType, $entityId, $payload = [], $priority = 5, $scheduledAt = null) { try { if ($scheduledAt === null) { $scheduledAt = date('Y-m-d H:i:s'); } $data = [ 'task_type' => $taskType, 'entity_type' => $entityType, 'entity_id' => (int)$entityId, 'priority' => $this->clampPriority((int)$priority), 'payload' => !empty($payload) ? json_encode($payload) : null, 'status' => 'pending', 'attempts' => 0, 'max_attempts' => 3, 'scheduled_at' => $scheduledAt, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; // Validate data $validationErrors = $this->validateTaskData($data); if (!empty($validationErrors)) { throw new Exception('Validation failed: ' . implode(', ', $validationErrors)); } // Check for duplicate pending tasks if ($this->hasPendingTask($entityType, $entityId, $taskType)) { log_message('info', "Duplicate task ignored: {$taskType} for {$entityType} #{$entityId}"); return false; } $result = $this->db->insert($this->table, $data); if ($result) { $taskId = $this->db->insert_id(); $this->logDatabaseOperation('create', $this->table, $data, $taskId); return $taskId; } return false; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue add task error: ' . $e->getMessage()); return false; } } /** * Compatibility method for CodeIgniter snake_case convention * * @param mixed $taskData Task data (array or individual parameters) * @return int|false Task ID or false on failure */ public function add_task($taskData) { // Handle both array and individual parameter formats if (is_array($taskData)) { return $this->addTask( $taskData['task_type'] ?? $taskData['type'], $taskData['entity_type'], $taskData['entity_id'], $taskData['payload'] ?? [], $taskData['priority'] ?? 5, $taskData['scheduled_at'] ?? null ); } else { // Legacy signature with individual parameters $args = func_get_args(); return $this->addTask( $args[0] ?? '', // task_type $args[1] ?? '', // entity_type $args[2] ?? 0, // entity_id $args[3] ?? [], // payload $args[4] ?? 5, // priority $args[5] ?? null // scheduled_at ); } } /** * Get next pending tasks for processing * * @param int $limit Maximum number of tasks to retrieve * @param array $taskTypes Optional filter by task types * @return array Array of task objects */ public function getNextTasks($limit = 10, $taskTypes = null) { try { $this->db->where('status', 'pending') ->where('scheduled_at <=', date('Y-m-d H:i:s')); if ($taskTypes !== null && is_array($taskTypes)) { $this->db->where_in('task_type', $taskTypes); } $query = $this->db->order_by('priority', 'ASC') ->order_by('scheduled_at', 'ASC') ->limit($limit) ->get($this->table); return $query->result(); } catch (Exception $e) { log_message('error', 'Desk-Moloni queue get next tasks error: ' . $e->getMessage()); return []; } } /** * Start processing a task * * @param int $taskId Task ID * @return bool Success status */ public function startTask($taskId) { try { $data = [ 'status' => 'processing', 'started_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; $result = $this->db->where('id', (int)$taskId) ->where('status', 'pending') // Only start if still pending ->update($this->table, $data); if ($result && $this->db->affected_rows() > 0) { $this->logDatabaseOperation('update', $this->table, $data, $taskId); return true; } return false; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue start task error: ' . $e->getMessage()); return false; } } /** * Complete a task successfully * * @param int $taskId Task ID * @param array $result Optional result data * @return bool Success status */ public function completeTask($taskId, $result = null) { try { $data = [ 'status' => 'completed', 'completed_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; // Add result to payload if provided if ($result !== null) { $task = $this->getTask($taskId); if ($task) { $payloadData = json_decode($task->payload, true) ?: []; $payloadData['result'] = $result; $data['payload'] = json_encode($payloadData); } } $updateResult = $this->db->where('id', (int)$taskId) ->where('status', 'processing') // Only complete if processing ->update($this->table, $data); if ($updateResult && $this->db->affected_rows() > 0) { $this->logDatabaseOperation('update', $this->table, $data, $taskId); return true; } return false; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue complete task error: ' . $e->getMessage()); return false; } } /** * Mark task as failed * * @param int $taskId Task ID * @param string $errorMessage Error message * @param bool $retry Whether to schedule for retry * @return bool Success status */ public function failTask($taskId, $errorMessage, $retry = true) { try { $task = $this->getTask($taskId); if (!$task) { return false; } $newAttempts = $task->attempts + 1; $shouldRetry = $retry && $newAttempts < $task->max_attempts; $data = [ 'attempts' => $newAttempts, 'error_message' => $errorMessage, 'updated_at' => date('Y-m-d H:i:s') ]; if ($shouldRetry) { // Schedule for retry with exponential backoff $retryDelay = min(pow(2, $newAttempts) * 60, 3600); // Max 1 hour delay $data['status'] = 'retry'; $data['scheduled_at'] = date('Y-m-d H:i:s', time() + $retryDelay); } else { // Mark as failed $data['status'] = 'failed'; $data['completed_at'] = date('Y-m-d H:i:s'); } $result = $this->db->where('id', (int)$taskId)->update($this->table, $data); if ($result) { $this->logDatabaseOperation('update', $this->table, $data, $taskId); } return $result; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue fail task error: ' . $e->getMessage()); return false; } } /** * Reset retry tasks to pending status * * @return int Number of tasks reset */ public function resetRetryTasks() { try { $data = [ 'status' => 'pending', 'updated_at' => date('Y-m-d H:i:s') ]; $result = $this->db->where('status', 'retry') ->where('scheduled_at <=', date('Y-m-d H:i:s')) ->update($this->table, $data); return $this->db->affected_rows(); } catch (Exception $e) { log_message('error', 'Desk-Moloni queue reset retry tasks error: ' . $e->getMessage()); return 0; } } /** * Get task by ID * * @param int $taskId Task ID * @return object|null Task object or null if not found */ public function getTask($taskId) { try { $query = $this->db->where('id', (int)$taskId)->get($this->table); return $query->num_rows() > 0 ? $query->row() : null; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue get task error: ' . $e->getMessage()); return null; } } /** * Get tasks by entity * * @param string $entityType Entity type * @param int $entityId Entity ID * @param string $status Optional status filter * @return array Array of task objects */ public function getTasksByEntity($entityType, $entityId, $status = null) { try { $this->db->where('entity_type', $entityType) ->where('entity_id', (int)$entityId); if ($status !== null) { $this->db->where('status', $status); } $query = $this->db->order_by('created_at', 'DESC')->get($this->table); return $query->result(); } catch (Exception $e) { log_message('error', 'Desk-Moloni queue get tasks by entity error: ' . $e->getMessage()); return []; } } /** * Cancel pending task * * @param int $taskId Task ID * @return bool Success status */ public function cancelTask($taskId) { try { $result = $this->db->where('id', (int)$taskId) ->where('status', 'pending') ->delete($this->table); if ($result && $this->db->affected_rows() > 0) { $this->logDatabaseOperation('delete', $this->table, ['id' => $taskId], $taskId); return true; } return false; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue cancel task error: ' . $e->getMessage()); return false; } } /** * Clean up old completed/failed tasks * * @param int $olderThanDays Delete tasks older than X days * @return int Number of tasks deleted */ public function cleanupOldTasks($olderThanDays = 30) { try { $cutoffDate = date('Y-m-d H:i:s', strtotime("-{$olderThanDays} days")); $result = $this->db->where_in('status', ['completed', 'failed']) ->where('completed_at <', $cutoffDate) ->delete($this->table); return $this->db->affected_rows(); } catch (Exception $e) { log_message('error', 'Desk-Moloni queue cleanup error: ' . $e->getMessage()); return 0; } } /** * Get queue statistics * * @return array Statistics array */ public function getStatistics() { try { $stats = []; // By status foreach ($this->validStatuses as $status) { $stats['by_status'][$status] = $this->db->where('status', $status) ->count_all_results($this->table); } // By task type foreach ($this->validTaskTypes as $taskType) { $stats['by_task_type'][$taskType] = $this->db->where('task_type', $taskType) ->count_all_results($this->table); } // By entity type foreach ($this->validEntityTypes as $entityType) { $stats['by_entity_type'][$entityType] = $this->db->where('entity_type', $entityType) ->count_all_results($this->table); } // Processing times (average for completed tasks in last 24 hours) $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours')); $query = $this->db->select('AVG(TIMESTAMPDIFF(SECOND, started_at, completed_at)) as avg_processing_time') ->where('status', 'completed') ->where('completed_at >', $yesterday) ->where('started_at IS NOT NULL') ->get($this->table); $stats['avg_processing_time_seconds'] = $query->row()->avg_processing_time ?: 0; // Failed tasks in last 24 hours $stats['failed_24h'] = $this->db->where('status', 'failed') ->where('completed_at >', $yesterday) ->count_all_results($this->table); return $stats; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue statistics error: ' . $e->getMessage()); return []; } } /** * Check if entity has pending task of specific type * * @param string $entityType Entity type * @param int $entityId Entity ID * @param string $taskType Task type * @return bool True if pending task exists */ public function hasPendingTask($entityType, $entityId, $taskType) { try { $count = $this->db->where('entity_type', $entityType) ->where('entity_id', (int)$entityId) ->where('task_type', $taskType) ->where('status', 'pending') ->count_all_results($this->table); return $count > 0; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue has pending task error: ' . $e->getMessage()); return false; } } /** * Update task priority * * @param int $taskId Task ID * @param int $priority New priority (1-9) * @return bool Success status */ public function updatePriority($taskId, $priority) { try { $priority = max($this->minPriority, min($this->maxPriority, (int)$priority)); $data = [ 'priority' => $priority, 'updated_at' => date('Y-m-d H:i:s') ]; $result = $this->db->where('id', (int)$taskId) ->where('status', 'pending') // Only update pending tasks ->update($this->table, $data); if ($result && $this->db->affected_rows() > 0) { $this->logDatabaseOperation('update', $this->table, $data, $taskId); return true; } return false; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue update priority error: ' . $e->getMessage()); return false; } } /** * Validate task data * * @param array $data Task data to validate * @return array Validation errors */ private function validateTaskData($data) { $errors = []; // Required fields $requiredFields = ['entity_type', 'entity_id', 'action']; $errors = array_merge($errors, $this->validateRequiredFields($data, $requiredFields)); // Task type validation (database schema) if (isset($data['task_type']) && !$this->validateEnum($data['task_type'], $this->validTaskTypes)) { $errors[] = 'Invalid task type. Must be one of: ' . implode(', ', $this->validTaskTypes); } // 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); } // Status validation if (isset($data['status']) && !$this->validateEnum($data['status'], $this->validStatuses)) { $errors[] = 'Invalid status. Must be one of: ' . implode(', ', $this->validStatuses); } // Priority validation if (isset($data['priority'])) { $priority = (int)$data['priority']; if ($priority < $this->minPriority || $priority > $this->maxPriority) { $errors[] = "Priority must be between {$this->minPriority} and {$this->maxPriority}"; } } // Entity ID validation if (isset($data['entity_id']) && (!is_numeric($data['entity_id']) || (int)$data['entity_id'] <= 0)) { $errors[] = 'Entity ID must be a positive integer'; } // JSON payload validation if (isset($data['payload']) && !$this->validateJSON($data['payload'])) { $errors[] = 'Payload must be valid JSON'; } return $errors; } /** * Get valid task types * * @return array Valid task types */ public function getValidTaskTypes() { return $this->validTaskTypes; } /** * Get valid entity types * * @return array Valid entity types */ public function getValidEntityTypes() { return $this->validEntityTypes; } /** * Get valid status values * * @return array Valid status values */ public function getValidStatuses() { return $this->validStatuses; } /** * Map task type to database action * * @param string $taskType Task type from API/tests * @return string Database action */ /** * Clamp numeric priority to valid range (1..9) */ private function clampPriority($priority) { return max($this->minPriority, min($this->maxPriority, (int)$priority)); } /** * Get count of items by status * * @param array $filters Filter criteria * @return int Count of items */ public function get_count($filters = []) { try { // Check if table exists first if (!$this->db->table_exists($this->table)) { log_message('info', 'Desk-Moloni sync queue table does not exist yet'); return 0; } // Reset any previous query builder state $this->db->reset_query(); $this->db->from($this->table); if (isset($filters['status'])) { $this->db->where('status', $filters['status']); } if (isset($filters['entity_type'])) { $this->db->where('entity_type', $filters['entity_type']); } if (isset($filters['priority'])) { $this->db->where('priority', $filters['priority']); } return $this->db->count_all_results(); } catch (Exception $e) { log_message('error', 'Desk-Moloni queue get_count error: ' . $e->getMessage()); return 0; } } /** * Get queue summary for dashboard * * @return array Queue summary statistics */ public function get_queue_summary() { try { $summary = []; // Get counts by status foreach (['pending', 'processing', 'completed', 'failed'] as $status) { $summary[$status] = $this->get_count(['status' => $status]); } // Get total items $summary['total'] = array_sum($summary); // Get recent activity (last 24 hours) $this->db->reset_query(); $this->db->from($this->table); $this->db->where('created_at >=', date('Y-m-d H:i:s', strtotime('-24 hours'))); $summary['recent_24h'] = $this->db->count_all_results(); return $summary; } catch (Exception $e) { log_message('error', 'Desk-Moloni queue summary error: ' . $e->getMessage()); return [ 'pending' => 0, 'processing' => 0, 'completed' => 0, 'failed' => 0, 'total' => 0, 'recent_24h' => 0 ]; } } }