Re-synced controllers, configs, libraries, seeds, and docs with the latest API expectations and response helpers.
390 lines
13 KiB
PHP
Executable File
390 lines
13 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Rule\RuleDefModel;
|
|
use App\Models\Test\TestDefSiteModel;
|
|
|
|
class RuleEngineService
|
|
{
|
|
protected RuleDefModel $ruleDefModel;
|
|
protected RuleExpressionService $expr;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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;
|
|
}
|
|
|
|
$testCode = $action['testCode'] ?? null;
|
|
if ($testCode !== null) {
|
|
$resolvedId = $this->resolveTestSiteIdByCode($testCode);
|
|
if ($resolvedId === null) {
|
|
throw new \Exception('SET_RESULT unknown test code: ' . $testCode);
|
|
}
|
|
$testSiteID = $resolvedId;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
$testSiteCode = $testCode ?? $this->resolveTestSiteCode($testSiteID);
|
|
|
|
$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' => $testSiteCode,
|
|
'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;
|
|
}
|
|
}
|
|
|
|
private function resolveTestSiteIdByCode(string $testSiteCode): ?int
|
|
{
|
|
try {
|
|
$testDefSiteModel = new TestDefSiteModel();
|
|
$row = $testDefSiteModel->where('TestSiteCode', $testSiteCode)->where('EndDate', null)->first();
|
|
if (empty($row['TestSiteID'])) {
|
|
return null;
|
|
}
|
|
|
|
return (int) $row['TestSiteID'];
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|