Files
desk-moloni/modules/desk_moloni/models/Desk_moloni_sync_queue_model.php
Emanuel Almeida c19f6fd9ee fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module
- Normalize hooks to after_client_* and instantiate PerfexHooks safely
- Fix OAuthController view path and API client class name
- Add missing admin views for webhook config/logs; adjust view loading
- Harden client portal routes and admin routes mapping
- Make Dashboard/Logs/Queue tolerant to optional model methods
- Align log details query with existing schema; avoid broken joins

This makes the module operational in Perfex (admin + client), reduces 404s,
and avoids fatal errors due to inconsistent tables/methods.
2025-09-11 17:38:45 +01:00

721 lines
23 KiB
PHP

<?php
/**
* Desk_moloni_sync_queue_model.php
*
* Model for desk_moloni_sync_queue table
* Handles asynchronous task queue for synchronization operations
*
* @package DeskMoloni\Models
* @author Database Design Specialist
* @version 3.0
*/
defined('BASEPATH') or exit('No direct script access allowed');
require_once(dirname(__FILE__) . '/Desk_moloni_model.php');
class Desk_moloni_sync_queue_model extends Desk_moloni_model
{
/**
* Table name - must match Perfex CRM naming convention
*/
private $table = 'tbldeskmoloni_sync_queue';
/**
* Valid task types (mapped from actions)
*/
private $validTaskTypes = [
'sync_client', 'sync_product', 'sync_invoice',
'sync_estimate', 'sync_credit_note', 'status_update'
];
/**
* Valid actions (database schema)
*/
// Kept for backward compatibility with older schemas (not used in current schema)
private $validActions = [
'create', 'update', 'delete', 'sync'
];
/**
* Valid entity types
*/
private $validEntityTypes = [
'client', 'product', 'invoice', 'estimate', 'credit_note'
];
/**
* Valid task status values
*/
private $validStatuses = [
'pending', 'processing', 'completed', 'failed', 'retry'
];
/**
* Maximum priority value
*/
private $maxPriority = 9;
/**
* Minimum priority value
*/
private $minPriority = 1;
public function __construct()
{
parent::__construct();
// Use Perfex CRM table naming convention: tbl + module_prefix + table_name
$this->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
];
}
}
}