feat: helpdesk automation engine + scheduled task plugin
Automation engine in TicketsModel: - Condition evaluator: field/op/value with eq, neq, gt, lt, in, not_in - Action executor: set_status, set_priority, assign, add_note - Trigger events: ticket_created, ticket_replied, status_changed, scheduled - Hooks wired into createTicket, addReply, updateStatus Scheduled task plugin (plg_task_mokowaas_tickets): - Runs all 'scheduled' automation rules against non-closed tickets - Evaluates age_hours, status, priority, sla_responded - Joomla Scheduler integration via TaskPluginTrait Default automation rules: 1. Auto-close resolved tickets after 7 days 2. Escalate urgent tickets with no response in 1 hour 3. Notify on high/urgent ticket creation Also: - Added #__mokowaas_ticket_automation table - Fixed dashboard ImportModel null error (direct instantiation) - Added task plugin to package manifest + script.php Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,23 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_canned` (
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_automation` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`trigger_event` VARCHAR(50) NOT NULL DEFAULT 'ticket_created',
|
||||
`conditions` TEXT NOT NULL DEFAULT '[]',
|
||||
`actions` TEXT NOT NULL DEFAULT '[]',
|
||||
`enabled` TINYINT NOT NULL DEFAULT 1,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Default automation rules
|
||||
INSERT IGNORE INTO `#__mokowaas_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES
|
||||
(1, 'Auto-close resolved tickets after 7 days', 'scheduled', '[{"field":"status","op":"eq","value":"resolved"},{"field":"age_hours","op":"gt","value":"168"}]', '[{"type":"set_status","value":"closed"},{"type":"add_note","value":"Auto-closed after 7 days with no response."}]', 1, 1),
|
||||
(2, 'Escalate urgent tickets with no response in 1 hour', 'scheduled', '[{"field":"priority","op":"eq","value":"urgent"},{"field":"sla_responded","op":"eq","value":"0"},{"field":"age_hours","op":"gt","value":"1"}]', '[{"type":"add_note","value":"SLA BREACH: Urgent ticket has no staff response after 1 hour."}]', 1, 2),
|
||||
(3, 'Notify on high priority ticket creation', 'ticket_created', '[{"field":"priority","op":"in","value":"high,urgent"}]', '[{"type":"add_note","value":"High/urgent ticket created — requires immediate attention."}]', 1, 3);
|
||||
|
||||
-- Default categories
|
||||
INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `description`, `sla_response_minutes`, `sla_resolution_minutes`, `ordering`) VALUES
|
||||
(1, 'General Support', 'general-support', 'General questions and assistance', 480, 2880, 1),
|
||||
|
||||
@@ -173,6 +173,9 @@ class TicketsModel extends BaseDatabaseModel
|
||||
|
||||
$db->insertObject('#__mokowaas_tickets', $ticket, 'id');
|
||||
|
||||
// Run automation
|
||||
$this->runAutomation('ticket_created', (int) $ticket->id);
|
||||
|
||||
return ['success' => true, 'message' => 'Ticket #' . $ticket->id . ' created.', 'id' => (int) $ticket->id];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
@@ -212,6 +215,9 @@ class TicketsModel extends BaseDatabaseModel
|
||||
->where($db->quoteName('sla_responded') . ' = 0')
|
||||
)->execute();
|
||||
|
||||
// Run automation
|
||||
$this->runAutomation('ticket_replied', $ticketId);
|
||||
|
||||
return ['success' => true, 'message' => 'Reply added.'];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
@@ -259,6 +265,9 @@ class TicketsModel extends BaseDatabaseModel
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
)->execute();
|
||||
|
||||
// Run automation
|
||||
$this->runAutomation('status_changed', $ticketId);
|
||||
|
||||
return ['success' => true, 'message' => 'Status updated to ' . $status . '.'];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
@@ -351,6 +360,235 @@ class TicketsModel extends BaseDatabaseModel
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Automation Engine
|
||||
// ==================================================================
|
||||
|
||||
/**
|
||||
* Run automation rules for a specific trigger event against a ticket.
|
||||
*
|
||||
* @param string $event trigger_event: ticket_created, ticket_replied, status_changed, scheduled
|
||||
* @param int $ticketId The ticket to evaluate
|
||||
*/
|
||||
public function runAutomation(string $event, int $ticketId): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Load enabled rules for this event
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_ticket_automation'))
|
||||
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
|
||||
->where($db->quoteName('enabled') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
$rules = $db->loadObjectList() ?: [];
|
||||
|
||||
if (empty($rules))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the ticket
|
||||
$ticket = $this->getTicket($ticketId);
|
||||
|
||||
if (!$ticket)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate age in hours
|
||||
$ticket->age_hours = (time() - strtotime($ticket->created)) / 3600;
|
||||
|
||||
foreach ($rules as $rule)
|
||||
{
|
||||
$conditions = json_decode($rule->conditions, true) ?: [];
|
||||
$actions = json_decode($rule->actions, true) ?: [];
|
||||
|
||||
if ($this->evaluateConditions($conditions, $ticket))
|
||||
{
|
||||
$this->executeActions($actions, $ticketId, $ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
\Joomla\CMS\Log\Log::add('Automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all scheduled automation rules against all open tickets.
|
||||
*/
|
||||
public function runScheduledAutomation(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$results = ['evaluated' => 0, 'acted' => 0];
|
||||
|
||||
// Load scheduled rules
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_ticket_automation'))
|
||||
->where($db->quoteName('trigger_event') . ' = ' . $db->quote('scheduled'))
|
||||
->where($db->quoteName('enabled') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
$rules = $db->loadObjectList() ?: [];
|
||||
|
||||
if (empty($rules))
|
||||
{
|
||||
return $results;
|
||||
}
|
||||
|
||||
// Load all non-closed tickets
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('status') . ' != ' . $db->quote('closed'));
|
||||
$db->setQuery($query);
|
||||
$tickets = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($tickets as $ticket)
|
||||
{
|
||||
$ticket->age_hours = (time() - strtotime($ticket->created)) / 3600;
|
||||
$ticket->replies = [];
|
||||
$results['evaluated']++;
|
||||
|
||||
foreach ($rules as $rule)
|
||||
{
|
||||
$conditions = json_decode($rule->conditions, true) ?: [];
|
||||
$actions = json_decode($rule->actions, true) ?: [];
|
||||
|
||||
if ($this->evaluateConditions($conditions, $ticket))
|
||||
{
|
||||
$this->executeActions($actions, (int) $ticket->id, $ticket);
|
||||
$results['acted']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a set of conditions against a ticket (all must match).
|
||||
*/
|
||||
private function evaluateConditions(array $conditions, object $ticket): bool
|
||||
{
|
||||
foreach ($conditions as $cond)
|
||||
{
|
||||
$field = $cond['field'] ?? '';
|
||||
$op = $cond['op'] ?? 'eq';
|
||||
$value = $cond['value'] ?? '';
|
||||
|
||||
$ticketValue = $ticket->{$field} ?? null;
|
||||
|
||||
if ($ticketValue === null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($op)
|
||||
{
|
||||
case 'eq':
|
||||
if ((string) $ticketValue !== (string) $value) return false;
|
||||
break;
|
||||
case 'neq':
|
||||
if ((string) $ticketValue === (string) $value) return false;
|
||||
break;
|
||||
case 'gt':
|
||||
if ((float) $ticketValue <= (float) $value) return false;
|
||||
break;
|
||||
case 'lt':
|
||||
if ((float) $ticketValue >= (float) $value) return false;
|
||||
break;
|
||||
case 'in':
|
||||
$list = array_map('trim', explode(',', $value));
|
||||
if (!\in_array((string) $ticketValue, $list, true)) return false;
|
||||
break;
|
||||
case 'not_in':
|
||||
$list = array_map('trim', explode(',', $value));
|
||||
if (\in_array((string) $ticketValue, $list, true)) return false;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a set of actions on a ticket.
|
||||
*/
|
||||
private function executeActions(array $actions, int $ticketId, object $ticket): void
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
foreach ($actions as $action)
|
||||
{
|
||||
$type = $action['type'] ?? '';
|
||||
$value = $action['value'] ?? '';
|
||||
|
||||
switch ($type)
|
||||
{
|
||||
case 'set_status':
|
||||
$this->updateStatus($ticketId, $value);
|
||||
break;
|
||||
|
||||
case 'set_priority':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('priority') . ' = ' . $db->quote($value))
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
)->execute();
|
||||
break;
|
||||
|
||||
case 'assign':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('assigned_to') . ' = ' . (int) $value)
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
)->execute();
|
||||
break;
|
||||
|
||||
case 'add_note':
|
||||
$reply = (object) [
|
||||
'ticket_id' => $ticketId,
|
||||
'user_id' => 0,
|
||||
'body' => $value,
|
||||
'is_internal' => 1,
|
||||
'created' => $now,
|
||||
];
|
||||
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all automation rules.
|
||||
*/
|
||||
public function getAutomationRules(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_ticket_automation'))
|
||||
->order($db->quoteName('ordering') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Akeeba Ticket System Importer
|
||||
// ==================================================================
|
||||
|
||||
@@ -36,9 +36,17 @@ class HtmlView extends BaseHtmlView
|
||||
$this->wafBlocks = $model->getRecentWafBlocks(5);
|
||||
|
||||
// Check for importable Akeeba data
|
||||
$importModel = $this->getModel('Import');
|
||||
$this->adminToolsAvailable = $importModel->checkAdminToolsAvailable();
|
||||
$this->atsAvailable = $importModel->checkAtsAvailable();
|
||||
try
|
||||
{
|
||||
$importModel = new \Moko\Component\MokoWaaS\Administrator\Model\ImportModel();
|
||||
$this->adminToolsAvailable = $importModel->checkAdminToolsAvailable();
|
||||
$this->atsAvailable = $importModel->checkAtsAvailable();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->adminToolsAvailable = null;
|
||||
$this->atsAvailable = null;
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
PLG_TASK_MOKOWAAS_TICKETS="Task - MokoWaaS Ticket Automation"
|
||||
PLG_TASK_MOKOWAAS_TICKETS_DESC="Runs scheduled helpdesk automation rules."
|
||||
PLG_TASK_MOKOWAAS_TICKETS_AUTOMATION_TITLE="MokoWaaS: Ticket Automation"
|
||||
PLG_TASK_MOKOWAAS_TICKETS_AUTOMATION_DESC="Runs time-based automation rules against open tickets (auto-close, SLA escalation, etc.)."
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
PLG_TASK_MOKOWAAS_TICKETS="Task - MokoWaaS Ticket Automation"
|
||||
PLG_TASK_MOKOWAAS_TICKETS_DESC="Runs scheduled helpdesk automation rules — auto-close, SLA escalation, and time-based actions."
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoWaaS Ticket Automation</name>
|
||||
<element>mokowaas_tickets</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.32.00</version>
|
||||
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoWaaSTickets</namespace>
|
||||
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_task_mokowaas_tickets.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokowaas_tickets.sys.ini</language>
|
||||
</languages>
|
||||
</extension>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Moko\Plugin\Task\MokoWaaSTickets\Extension\TicketAutomation;
|
||||
|
||||
return new class implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$dispatcher = $container->get(DispatcherInterface::class);
|
||||
$plugin = new TicketAutomation($dispatcher, (array) PluginHelper::getPlugin('task', 'mokowaas_tickets'));
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_task_mokowaas_tickets
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\Task\MokoWaaSTickets\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Status;
|
||||
use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
use Moko\Component\MokoWaaS\Administrator\Model\TicketsModel;
|
||||
|
||||
class TicketAutomation extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
use TaskPluginTrait;
|
||||
|
||||
protected const TASKS_MAP = [
|
||||
'mokowaas.ticket.automation' => [
|
||||
'langConstPrefix' => 'PLG_TASK_MOKOWAAS_TICKETS_AUTOMATION',
|
||||
'method' => 'runAutomation',
|
||||
],
|
||||
];
|
||||
|
||||
protected $autoloadLanguage = true;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'onTaskOptionsList' => 'advertiseRoutines',
|
||||
'onExecuteTask' => 'standardRoutineHandler',
|
||||
'onContentPrepareForm' => 'enhanceTaskItemForm',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all scheduled automation rules against open tickets.
|
||||
*/
|
||||
private function runAutomation(ExecuteTaskEvent $event): int
|
||||
{
|
||||
try
|
||||
{
|
||||
$model = new TicketsModel();
|
||||
$results = $model->runScheduledAutomation();
|
||||
|
||||
$this->logTask(
|
||||
\sprintf('Ticket automation: evaluated %d tickets, acted on %d', $results['evaluated'], $results['acted'])
|
||||
);
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->logTask('Ticket automation failed: ' . $e->getMessage(), 'error');
|
||||
|
||||
return Status::KNOCKOUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
|
||||
<file type="plugin" id="plg_task_mokowaasdemo" group="task">plg_task_mokowaasdemo.zip</file>
|
||||
<file type="plugin" id="plg_task_mokowaassync" group="task">plg_task_mokowaassync.zip</file>
|
||||
<file type="plugin" id="plg_task_mokowaas_tickets" group="task">plg_task_mokowaas_tickets.zip</file>
|
||||
<file type="template" id="mokoonyx" client="site">tpl_mokoonyx.zip</file>
|
||||
</files>
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class Pkg_MokowaasInstallerScript
|
||||
$this->enablePlugin('webservices', 'mokowaas');
|
||||
$this->enablePlugin('task', 'mokowaasdemo');
|
||||
$this->enablePlugin('task', 'mokowaassync');
|
||||
$this->enablePlugin('task', 'mokowaas_tickets');
|
||||
|
||||
// Migrate params from core plugin to feature plugins (one-time)
|
||||
$this->migrateFeatureParams();
|
||||
@@ -411,6 +412,7 @@ class Pkg_MokowaasInstallerScript
|
||||
$db->quote('mod_mokowaas_cpanel'),
|
||||
$db->quote('mokowaasdemo'),
|
||||
$db->quote('mokowaassync'),
|
||||
$db->quote('mokowaas_tickets'),
|
||||
$db->quote('perfectpublisher'),
|
||||
$db->quote('mokoonyx'),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user