27f50468a8
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Successful in 14s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 32s
Generic: Project CI / Lint & Validate (pull_request) Successful in 34s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 34s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 35s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Merges the full element rename from main into dev, resolving conflicts across 53 files. Migrates new dev-only extensions (com_mokosuite ticketsettings, plg_system_mokosuite_dbip) to MokoSuiteClient naming.
242 lines
7.0 KiB
PHP
242 lines
7.0 KiB
PHP
<?php
|
|
/**
|
|
* @package MokoSuiteClient
|
|
* @subpackage com_mokosuiteclient
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Moko\Component\MokoSuiteClient\Administrator\Service;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Log\Log;
|
|
|
|
/**
|
|
* Automation rule engine — evaluates trigger/condition/action rules.
|
|
*
|
|
* Called from event hooks (system plugin, task plugin) whenever
|
|
* a triggering event occurs. Loads matching rules, checks conditions,
|
|
* and executes actions.
|
|
*
|
|
* @since 02.35.00
|
|
*/
|
|
class AutomationEngine
|
|
{
|
|
/**
|
|
* Fire all matching rules for a given trigger event.
|
|
*
|
|
* @param string $triggerEvent Event name (ticket_created, user_login, etc.)
|
|
* @param array $context Context data (ticket object, user data, etc.)
|
|
*/
|
|
public static function fire(string $triggerEvent, array $context = []): void
|
|
{
|
|
try
|
|
{
|
|
$rules = self::getActiveRules($triggerEvent);
|
|
|
|
foreach ($rules as $rule)
|
|
{
|
|
$conditions = json_decode($rule->conditions, true) ?: [];
|
|
$actions = json_decode($rule->actions, true) ?: [];
|
|
|
|
if (self::evaluateConditions($conditions, $context))
|
|
{
|
|
self::executeActions($actions, $rule, $context);
|
|
}
|
|
}
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
Log::add('Automation engine error: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get active automation rules for a trigger event.
|
|
*/
|
|
private static function getActiveRules(string $event): array
|
|
{
|
|
$db = Factory::getDbo();
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select('*')
|
|
->from('#__mokosuiteclient_ticket_automation')
|
|
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
|
|
->where($db->quoteName('enabled') . ' = 1')
|
|
->order('ordering ASC')
|
|
);
|
|
return $db->loadObjectList() ?: [];
|
|
}
|
|
|
|
/**
|
|
* Evaluate all conditions (AND logic).
|
|
*/
|
|
private static function evaluateConditions(array $conditions, array $context): bool
|
|
{
|
|
foreach ($conditions as $c)
|
|
{
|
|
$field = $c['field'] ?? '';
|
|
$op = $c['op'] ?? 'eq';
|
|
$expected = $c['value'] ?? '';
|
|
$actual = $context[$field] ?? '';
|
|
|
|
switch ($op)
|
|
{
|
|
case 'eq': if ((string) $actual !== (string) $expected) return false; break;
|
|
case 'neq': if ((string) $actual === (string) $expected) return false; break;
|
|
case 'gt': if ((float) $actual <= (float) $expected) return false; break;
|
|
case 'lt': if ((float) $actual >= (float) $expected) return false; break;
|
|
case 'in':
|
|
$values = array_map('trim', explode(',', $expected));
|
|
if (!in_array((string) $actual, $values, true)) return false;
|
|
break;
|
|
case 'not_in':
|
|
$values = array_map('trim', explode(',', $expected));
|
|
if (in_array((string) $actual, $values, true)) return false;
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Execute actions for a matched rule.
|
|
*/
|
|
private static function executeActions(array $actions, object $rule, array $context): void
|
|
{
|
|
$db = Factory::getDbo();
|
|
$ticketId = (int) ($context['ticket_id'] ?? $context['id'] ?? 0);
|
|
|
|
foreach ($actions as $action)
|
|
{
|
|
$type = $action['type'] ?? '';
|
|
$value = $action['value'] ?? '';
|
|
|
|
try
|
|
{
|
|
switch ($type)
|
|
{
|
|
case 'set_status':
|
|
if ($ticketId) {
|
|
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET status = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
|
|
}
|
|
break;
|
|
|
|
case 'set_priority':
|
|
if ($ticketId) {
|
|
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET priority = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
|
|
}
|
|
break;
|
|
|
|
case 'assign':
|
|
if ($ticketId) {
|
|
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET assigned_to = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
|
|
}
|
|
break;
|
|
|
|
case 'add_note':
|
|
if ($ticketId) {
|
|
$note = (object) [
|
|
'ticket_id' => $ticketId,
|
|
'user_id' => 0,
|
|
'body' => $value ?: '[Automation: ' . ($rule->title ?? '') . ']',
|
|
'is_internal' => 1,
|
|
'created' => Factory::getDate()->toSql(),
|
|
];
|
|
$db->insertObject('#__mokosuiteclient_ticket_replies', $note);
|
|
}
|
|
break;
|
|
|
|
case 'send_email':
|
|
NotificationService::securityAlert(
|
|
'automation',
|
|
'Automation: ' . ($rule->title ?? ''),
|
|
$value ?: 'Rule triggered for ticket #' . $ticketId
|
|
);
|
|
break;
|
|
|
|
case 'send_ntfy':
|
|
NotificationService::pushNtfySecurity(
|
|
'automation',
|
|
'Automation: ' . ($rule->title ?? ''),
|
|
$value ?: 'Rule triggered for ticket #' . $ticketId
|
|
);
|
|
break;
|
|
|
|
case 'close':
|
|
if ($ticketId) {
|
|
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
|
|
}
|
|
break;
|
|
|
|
case 'create_ticket':
|
|
self::createTicketFromAutomation($rule, $context, $value);
|
|
break;
|
|
}
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
Log::add("Automation action {$type} failed: " . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a ticket from automation (with behavior: append/always_new/skip_if_open).
|
|
*/
|
|
private static function createTicketFromAutomation(object $rule, array $context, string $subject): void
|
|
{
|
|
$db = Factory::getDbo();
|
|
$behavior = $rule->behavior ?? 'append';
|
|
$userId = (int) ($context['user_id'] ?? 0);
|
|
$catId = (int) ($context['category_id'] ?? 0);
|
|
|
|
if ($behavior !== 'always_new' && $userId > 0)
|
|
{
|
|
// Check for existing open ticket
|
|
$query = $db->getQuery(true)
|
|
->select('id')
|
|
->from('#__mokosuiteclient_tickets')
|
|
->where('created_by = ' . $userId)
|
|
->where("status NOT IN ('closed', 'resolved')");
|
|
|
|
if ($catId > 0) {
|
|
$query->where('category_id = ' . $catId);
|
|
}
|
|
|
|
$db->setQuery($query, 0, 1);
|
|
$existingId = (int) $db->loadResult();
|
|
|
|
if ($existingId > 0)
|
|
{
|
|
if ($behavior === 'skip_if_open') return;
|
|
|
|
// append — add reply to existing ticket
|
|
$reply = (object) [
|
|
'ticket_id' => $existingId,
|
|
'user_id' => 0,
|
|
'body' => $subject ?: '[Automation: ' . ($rule->title ?? '') . ']',
|
|
'is_internal' => 1,
|
|
'created' => Factory::getDate()->toSql(),
|
|
];
|
|
$db->insertObject('#__mokosuiteclient_ticket_replies', $reply);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Create new ticket
|
|
$ticket = (object) [
|
|
'subject' => $subject ?: 'Automation: ' . ($rule->title ?? ''),
|
|
'body' => $context['body'] ?? '',
|
|
'status' => 'open',
|
|
'priority' => $context['priority'] ?? 'normal',
|
|
'category_id' => $catId ?: null,
|
|
'created_by' => $userId,
|
|
'created' => Factory::getDate()->toSql(),
|
|
];
|
|
$db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id');
|
|
}
|
|
}
|