ruleDefModel = new RuleDefModel(); $this->expr = new RuleExpressionService(); } /** * Run rules for an event. * * Expected context keys: * - order: array (must include InternalOID) * - patient: array (optional, includes Sex, Age, etc.) * - tests: array (optional, patres rows) * * @param string $eventCode The event that triggered rule execution * @param array $context Context data for rule evaluation * @return void */ public function run(string $eventCode, array $context = []): void { $order = $context['order'] ?? null; $testSiteID = $context['testSiteID'] ?? null; if (is_array($order) && isset($order['TestSiteID']) && $testSiteID === null) { $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null; } $rules = $this->ruleDefModel->getActiveByEvent($eventCode, $testSiteID); if (empty($rules)) { return; } foreach ($rules as $rule) { $rid = (int) ($rule['RuleID'] ?? 0); if ($rid === 0) { continue; } try { // Rules must have compiled expressions $compiled = null; if (!empty($rule['ConditionExprCompiled'])) { $compiled = json_decode($rule['ConditionExprCompiled'], true); } if (empty($compiled) || !is_array($compiled)) { log_message('warning', 'Rule ' . $rid . ' has no compiled expression, skipping'); continue; } // Evaluate condition $conditionExpr = $compiled['conditionExpr'] ?? 'true'; $matches = $this->evaluateCondition($conditionExpr, $context); if (!$matches) { // Execute else actions $actions = $compiled['else'] ?? []; } else { // Execute then actions $actions = $compiled['then'] ?? []; } // Execute all actions foreach ($actions as $action) { $this->executeAction($action, $context); } } catch (\Throwable $e) { log_message('error', 'Rule engine error (RuleID=' . $rid . '): ' . $e->getMessage()); continue; } } } /** * Evaluate a condition expression * Handles special functions like requested() by querying the database */ private function evaluateCondition(string $conditionExpr, array $context): bool { // Handle requested() function(s) by querying database $conditionExpr = preg_replace_callback( '/requested\s*\(\s*["\']([^"\']+)["\']\s*\)/i', function (array $m) use ($context) { $testCode = $m[1] ?? ''; if ($testCode === '') { return 'false'; } return $this->isTestRequested($testCode, $context) ? 'true' : 'false'; }, $conditionExpr ); return $this->expr->evaluateBoolean($conditionExpr, $context); } /** * Check if a test was requested for the current order */ private function isTestRequested(string $testCode, array $context): bool { $order = $context['order'] ?? null; if (!is_array($order) || empty($order['InternalOID'])) { return false; } $internalOID = (int) $order['InternalOID']; $db = \Config\Database::connect(); // Query patres to check if test with given code exists $result = $db->table('patres') ->select('patres.*, testdefsite.TestSiteCode') ->join('testdefsite', 'testdefsite.TestSiteID = patres.TestSiteID', 'inner') ->where('patres.OrderID', $internalOID) ->where('testdefsite.TestSiteCode', $testCode) ->where('patres.DelDate', null) ->where('testdefsite.EndDate', null) ->get() ->getRow(); return $result !== null; } /** * Execute an action based on its type */ protected function executeAction(array $action, array $context): void { $type = strtoupper((string) ($action['type'] ?? '')); switch ($type) { case 'RESULT_SET': case 'SET_RESULT': // legacy $this->executeSetResult($action, $context); break; case 'TEST_INSERT': case 'INSERT_TEST': // legacy $this->executeInsertTest($action, $context); break; case 'TEST_DELETE': $this->executeDeleteTest($action, $context); break; case 'COMMENT_INSERT': case 'ADD_COMMENT': // legacy $this->executeAddComment($action, $context); break; case 'NO_OP': // Do nothing break; default: log_message('warning', 'Unknown action type: ' . $type); break; } } /** * Execute SET_RESULT action */ protected function executeSetResult(array $action, array $context): void { $order = $context['order'] ?? null; if (!is_array($order) || empty($order['InternalOID'])) { throw new \Exception('SET_RESULT requires context.order.InternalOID'); } $internalOID = (int) $order['InternalOID']; $testSiteID = $context['testSiteID'] ?? null; if ($testSiteID === null && isset($order['TestSiteID'])) { $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null; } if ($testSiteID === null) { throw new \Exception('SET_RESULT requires testSiteID'); } // Get the value if (isset($action['valueExpr']) && is_string($action['valueExpr'])) { $value = $this->expr->evaluate($action['valueExpr'], $context); } else { $value = $action['value'] ?? null; } $db = \Config\Database::connect(); // Check if patres row exists $patres = $db->table('patres') ->where('OrderID', $internalOID) ->where('TestSiteID', $testSiteID) ->where('DelDate', null) ->get() ->getRowArray(); if ($patres) { // Update existing result $ok = $db->table('patres') ->where('OrderID', $internalOID) ->where('TestSiteID', $testSiteID) ->where('DelDate', null) ->update(['Result' => $value]); } else { // Insert new result row $ok = $db->table('patres')->insert([ 'OrderID' => $internalOID, 'TestSiteID' => $testSiteID, 'TestSiteCode' => $this->resolveTestSiteCode($testSiteID), 'Result' => $value, 'CreateDate' => date('Y-m-d H:i:s'), ]); } if ($ok === false) { throw new \Exception('SET_RESULT update/insert failed'); } } /** * Execute INSERT_TEST action - Insert a new test into patres */ protected function executeInsertTest(array $action, array $context): void { $order = $context['order'] ?? null; if (!is_array($order) || empty($order['InternalOID'])) { throw new \Exception('INSERT_TEST requires context.order.InternalOID'); } $internalOID = (int) $order['InternalOID']; $testCode = $action['testCode'] ?? null; if (empty($testCode)) { throw new \Exception('INSERT_TEST requires testCode'); } // Look up TestSiteID from TestSiteCode $testDefSiteModel = new TestDefSiteModel(); $testSite = $testDefSiteModel->where('TestSiteCode', $testCode) ->where('EndDate', null) ->first(); if (!$testSite || empty($testSite['TestSiteID'])) { throw new \Exception('INSERT_TEST: Test not found with code: ' . $testCode); } $testSiteID = (int) $testSite['TestSiteID']; $db = \Config\Database::connect(); // Check if test already exists (avoid duplicates) $existing = $db->table('patres') ->where('OrderID', $internalOID) ->where('TestSiteID', $testSiteID) ->where('DelDate', null) ->get() ->getRow(); if ($existing) { // Test already exists, skip return; } // Insert new test row $ok = $db->table('patres')->insert([ 'OrderID' => $internalOID, 'TestSiteID' => $testSiteID, 'TestSiteCode' => $testCode, 'CreateDate' => date('Y-m-d H:i:s'), ]); if ($ok === false) { throw new \Exception('INSERT_TEST insert failed'); } } /** * Execute ADD_COMMENT action - Add a comment to the order */ protected function executeAddComment(array $action, array $context): void { $order = $context['order'] ?? null; if (!is_array($order) || empty($order['InternalOID'])) { throw new \Exception('ADD_COMMENT requires context.order.InternalOID'); } $internalOID = (int) $order['InternalOID']; $comment = $action['comment'] ?? null; if (empty($comment)) { throw new \Exception('ADD_COMMENT requires comment'); } $db = \Config\Database::connect(); // Insert comment into ordercom table $ok = $db->table('ordercom')->insert([ 'InternalOID' => $internalOID, 'Comment' => $comment, 'CreateDate' => date('Y-m-d H:i:s'), ]); if ($ok === false) { throw new \Exception('ADD_COMMENT insert failed'); } } /** * Execute TEST_DELETE action - soft delete a test from patres */ protected function executeDeleteTest(array $action, array $context): void { $order = $context['order'] ?? null; if (!is_array($order) || empty($order['InternalOID'])) { throw new \Exception('TEST_DELETE requires context.order.InternalOID'); } $internalOID = (int) $order['InternalOID']; $testCode = $action['testCode'] ?? null; if (empty($testCode)) { throw new \Exception('TEST_DELETE requires testCode'); } $testDefSiteModel = new TestDefSiteModel(); $testSite = $testDefSiteModel->where('TestSiteCode', $testCode) ->where('EndDate', null) ->first(); if (!$testSite || empty($testSite['TestSiteID'])) { // Unknown test code: no-op return; } $testSiteID = (int) $testSite['TestSiteID']; $db = \Config\Database::connect(); // Soft delete matching patres row(s) $db->table('patres') ->where('OrderID', $internalOID) ->where('TestSiteID', $testSiteID) ->where('DelDate', null) ->update(['DelDate' => date('Y-m-d H:i:s')]); } private function resolveTestSiteCode(int $testSiteID): ?string { try { $testDefSiteModel = new TestDefSiteModel(); $row = $testDefSiteModel->where('TestSiteID', $testSiteID)->where('EndDate', null)->first(); return $row['TestSiteCode'] ?? null; } catch (\Throwable $e) { return null; } } }