From 0620ffd73556c46c906ec921e371ef4e794083e6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 07:08:59 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20expanded=20automation=20=E2=80=94=20Joo?= =?UTF-8?q?mla=20event=20triggers,=20create=5Fticket=20action,=20behavior?= =?UTF-8?q?=20options=20(#151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New trigger events hooked into core plugin: - user_login — fires on successful Joomla login - user_register — fires on new user creation - user_login_failed — fires on failed login attempt New action type: create_ticket with behavior options: - append: add reply to existing open ticket (same user+category) - always_new: always create a new ticket - skip_if_open: do nothing if open ticket exists New method: runSystemEventAutomation() for non-ticket events that builds a virtual context object from event data. Automation rules can now create tickets from any system event, with intelligent deduplication to avoid ticket spam. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../admin/src/Model/TicketsModel.php | 106 ++++++++++++++++++ .../Extension/MokoWaaS.php | 50 ++++++++- 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php b/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php index 9b7a8169..47c0ea5c 100644 --- a/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php +++ b/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php @@ -608,10 +608,116 @@ class TicketsModel extends BaseDatabaseModel } } break; + + case 'create_ticket': + // value = JSON: {"subject":"...","body":"...","category_id":1,"priority":"normal","behavior":"append"} + $ticketData = json_decode($value, true) ?: []; + $behavior = $ticketData['behavior'] ?? 'append'; + $userId = (int) ($ticket->created_by ?? 0); + $catId = (int) ($ticketData['category_id'] ?? 0); + + if ($behavior === 'append' && $userId > 0) + { + // Check for existing open ticket from this user in this category + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__mokowaas_tickets')) + ->where($db->quoteName('created_by') . ' = ' . $userId) + ->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')') + ->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1') + ->order($db->quoteName('created') . ' DESC') + ->setLimit(1) + ); + $existingId = (int) $db->loadResult(); + + if ($existingId) + { + $this->addReply($existingId, $ticketData['body'] ?? 'Automation event', true); + break; + } + } + elseif ($behavior === 'skip_if_open' && $userId > 0) + { + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokowaas_tickets')) + ->where($db->quoteName('created_by') . ' = ' . $userId) + ->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')') + ); + + if ((int) $db->loadResult() > 0) + { + break; + } + } + + // Create new ticket + $this->createTicket([ + 'subject' => $ticketData['subject'] ?? 'Automation: ' . ($ticket->subject ?? 'System event'), + 'body' => $ticketData['body'] ?? '', + 'priority' => $ticketData['priority'] ?? 'normal', + 'category_id' => $catId, + ]); + break; } } } + /** + * Run automation for a system event (not tied to a specific ticket). + * Creates a virtual ticket context from event data. + */ + public function runSystemEventAutomation(string $event, array $eventData = []): void + { + try + { + $db = $this->getDatabase(); + + $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; + } + + // Build a virtual ticket-like object from event data + $context = (object) array_merge([ + 'id' => 0, + 'subject' => $eventData['subject'] ?? $event, + 'body' => $eventData['body'] ?? '', + 'status' => 'open', + 'priority' => $eventData['priority'] ?? 'normal', + 'created_by' => $eventData['user_id'] ?? 0, + 'created' => gmdate('Y-m-d H:i:s'), + 'age_hours' => 0, + ], $eventData); + + foreach ($rules as $rule) + { + $conditions = json_decode($rule->conditions, true) ?: []; + $actions = json_decode($rule->actions, true) ?: []; + + if (empty($conditions) || $this->evaluateConditions($conditions, $context)) + { + $this->executeActions($actions, 0, $context); + } + } + } + catch (\Throwable $e) + { + \Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas'); + } + } + /** * Get all automation rules. */ diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 9aee79ec..39393f18 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -938,11 +938,57 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface return; } - // NOTE: warnMissingLicenseKey and enforceAdminRestrictions - // are now handled by feature plugins (deferred / tenant) $this->protectPlugin(); } + // ------------------------------------------------------------------ + // Automation event hooks (#151) — delegate to ticket automation engine + // ------------------------------------------------------------------ + + public function onUserLogin($user, $options = []) + { + $this->fireTicketAutomation('user_login', [ + 'user_id' => $user['id'] ?? 0, + 'username' => $user['username'] ?? '', + 'subject' => 'User login: ' . ($user['username'] ?? ''), + 'body' => 'User ' . ($user['username'] ?? '') . ' logged in from ' . ($_SERVER['REMOTE_ADDR'] ?? ''), + ]); + } + + public function onUserAfterSave($user, $isNew, $success, $msg) + { + if ($isNew && $success) + { + $this->fireTicketAutomation('user_register', [ + 'user_id' => $user['id'] ?? 0, + 'username' => $user['username'] ?? '', + 'subject' => 'New user registered: ' . ($user['username'] ?? ''), + 'body' => 'New user: ' . ($user['name'] ?? '') . ' (' . ($user['email'] ?? '') . ')', + ]); + } + } + + public function onUserLoginFailure($response) + { + $this->fireTicketAutomation('user_login_failed', [ + 'subject' => 'Failed login attempt', + 'body' => 'Failed login from ' . ($_SERVER['REMOTE_ADDR'] ?? '') . ': ' . ($response['username'] ?? ''), + ]); + } + + private function fireTicketAutomation(string $event, array $data): void + { + try + { + $model = new \Moko\Component\MokoWaaS\Administrator\Model\TicketsModel(); + $model->runSystemEventAutomation($event, $data); + } + catch (\Throwable $e) + { + // Silent — automation should never break the main flow + } + } + /** * Inject visual branding into the document head. *