2026-06-09 11:15:51 -05:00
<? php
/**
2026-06-16 13:09:14 -05:00
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
2026-06-09 11:15:51 -05:00
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
2026-06-16 13:09:14 -05:00
namespace Moko\Component\MokoSuiteClient\Administrator\Service ;
2026-06-09 11:15:51 -05:00
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 )
{
2026-06-16 13:09:14 -05:00
Log :: add ( 'Automation engine error: ' . $e -> getMessage (), Log :: ERROR , 'mokosuiteclient' );
2026-06-09 11:15:51 -05:00
}
}
/**
* Get active automation rules for a trigger event.
*/
private static function getActiveRules ( string $event ) : array
{
$db = Factory :: getDbo ();
$db -> setQuery (
$db -> getQuery ( true )
-> select ( '*' )
2026-06-16 13:09:14 -05:00
-> from ( '#__mokosuiteclient_ticket_automation' )
2026-06-09 11:15:51 -05:00
-> 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 ) {
2026-06-16 13:09:14 -05:00
$db -> setQuery ( "UPDATE { $db -> quoteName ( '#__mokosuiteclient_tickets' ) } SET status = { $db -> quote ( $value ) } , modified = { $db -> quote ( Factory :: getDate () -> toSql ()) } WHERE id = { $ticketId } " ) -> execute ();
2026-06-09 11:15:51 -05:00
}
break ;
case 'set_priority' :
if ( $ticketId ) {
2026-06-16 13:09:14 -05:00
$db -> setQuery ( "UPDATE { $db -> quoteName ( '#__mokosuiteclient_tickets' ) } SET priority = { $db -> quote ( $value ) } , modified = { $db -> quote ( Factory :: getDate () -> toSql ()) } WHERE id = { $ticketId } " ) -> execute ();
2026-06-09 11:15:51 -05:00
}
break ;
case 'assign' :
if ( $ticketId ) {
2026-06-16 13:09:14 -05:00
$db -> setQuery ( "UPDATE { $db -> quoteName ( '#__mokosuiteclient_tickets' ) } SET assigned_to = { $db -> quote ( $value ) } , modified = { $db -> quote ( Factory :: getDate () -> toSql ()) } WHERE id = { $ticketId } " ) -> execute ();
2026-06-09 11:15:51 -05:00
}
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 (),
];
2026-06-16 13:09:14 -05:00
$db -> insertObject ( '#__mokosuiteclient_ticket_replies' , $note );
2026-06-09 11:15:51 -05:00
}
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 ) {
2026-06-16 13:09:14 -05:00
$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 ();
2026-06-09 11:15:51 -05:00
}
break ;
case 'create_ticket' :
self :: createTicketFromAutomation ( $rule , $context , $value );
break ;
}
}
catch ( \Throwable $e )
{
2026-06-16 13:09:14 -05:00
Log :: add ( "Automation action { $type } failed: " . $e -> getMessage (), Log :: WARNING , 'mokosuiteclient' );
2026-06-09 11:15:51 -05:00
}
}
}
/**
* 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' )
2026-06-16 13:09:14 -05:00
-> from ( '#__mokosuiteclient_tickets' )
2026-06-09 11:15:51 -05:00
-> 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 (),
];
2026-06-16 13:09:14 -05:00
$db -> insertObject ( '#__mokosuiteclient_ticket_replies' , $reply );
2026-06-09 11:15:51 -05:00
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 (),
];
2026-06-16 13:09:14 -05:00
$db -> insertObject ( '#__mokosuiteclient_tickets' , $ticket , 'id' );
2026-06-09 11:15:51 -05:00
}
}