2026-03-12 06:34:56 +07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Rule\RuleActionModel;
|
|
|
|
|
use App\Models\Rule\RuleDefModel;
|
|
|
|
|
use App\Models\Test\TestDefSiteModel;
|
|
|
|
|
|
|
|
|
|
class RuleEngineService
|
|
|
|
|
{
|
|
|
|
|
protected RuleDefModel $ruleDefModel;
|
|
|
|
|
protected RuleActionModel $ruleActionModel;
|
|
|
|
|
protected RuleExpressionService $expr;
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->ruleDefModel = new RuleDefModel();
|
|
|
|
|
$this->ruleActionModel = new RuleActionModel();
|
|
|
|
|
$this->expr = new RuleExpressionService();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Run rules for an event.
|
|
|
|
|
*
|
|
|
|
|
* Expected context keys for ORDER_CREATED:
|
|
|
|
|
* - order: array (must include InternalOID)
|
|
|
|
|
* - tests: array (patres rows, optional)
|
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ruleIDs = array_values(array_filter(array_map(static fn ($r) => $r['RuleID'] ?? null, $rules)));
|
|
|
|
|
$actions = $this->ruleActionModel->getActiveByRuleIDs($ruleIDs);
|
|
|
|
|
|
|
|
|
|
$actionsByRule = [];
|
|
|
|
|
foreach ($actions as $action) {
|
|
|
|
|
$rid = $action['RuleID'] ?? null;
|
|
|
|
|
if (!$rid) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$actionsByRule[$rid][] = $action;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($rules as $rule) {
|
|
|
|
|
$rid = (int) ($rule['RuleID'] ?? 0);
|
|
|
|
|
if ($rid === 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-12 16:55:03 +07:00
|
|
|
// Check for compiled expression first
|
|
|
|
|
$compiled = null;
|
|
|
|
|
if (!empty($rule['ConditionExprCompiled'])) {
|
|
|
|
|
$compiled = json_decode($rule['ConditionExprCompiled'], true);
|
2026-03-12 06:34:56 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 16:55:03 +07:00
|
|
|
if (!empty($compiled) && is_array($compiled)) {
|
|
|
|
|
// Compiled rule: evaluate condition from compiled structure
|
|
|
|
|
$conditionExpr = $compiled['conditionExpr'] ?? 'true';
|
|
|
|
|
$matches = $this->expr->evaluateBoolean($conditionExpr, $context);
|
|
|
|
|
if (!$matches) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use compiled valueExpr for SET_RESULT action
|
|
|
|
|
if (!empty($compiled['valueExpr'])) {
|
|
|
|
|
$this->executeCompiledSetResult($rid, $compiled['valueExpr'], $context);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Legacy rule: evaluate raw ConditionExpr and execute stored actions
|
|
|
|
|
$matches = $this->expr->evaluateBoolean($rule['ConditionExpr'] ?? null, $context);
|
|
|
|
|
if (!$matches) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($actionsByRule[$rid] ?? [] as $action) {
|
|
|
|
|
$this->executeAction($action, $context);
|
|
|
|
|
}
|
2026-03-12 06:34:56 +07:00
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
log_message('error', 'Rule engine error (RuleID=' . $rid . '): ' . $e->getMessage());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 16:55:03 +07:00
|
|
|
/**
|
|
|
|
|
* Execute SET_RESULT action using compiled valueExpr.
|
|
|
|
|
* Automatically creates the test result if it doesn't exist.
|
|
|
|
|
*/
|
|
|
|
|
protected function executeCompiledSetResult(int $ruleID, string $valueExpr, 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) {
|
|
|
|
|
// Try to get testSiteID from context tests
|
|
|
|
|
$tests = $context['tests'] ?? [];
|
|
|
|
|
if (!empty($tests) && is_array($tests)) {
|
|
|
|
|
$testSiteID = (int) ($tests[0]['TestSiteID'] ?? null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($testSiteID === null) {
|
|
|
|
|
throw new \Exception('SET_RESULT requires testSiteID');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Evaluate the value expression
|
|
|
|
|
$value = $this->expr->evaluate($valueExpr, $context);
|
|
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
'Result' => $value,
|
|
|
|
|
'CreateDate' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($ok === false) {
|
|
|
|
|
throw new \Exception('SET_RESULT update/insert failed');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 06:34:56 +07:00
|
|
|
protected function executeAction(array $action, array $context): void
|
|
|
|
|
{
|
|
|
|
|
$type = strtoupper((string) ($action['ActionType'] ?? ''));
|
|
|
|
|
|
|
|
|
|
if ($type === 'SET_RESULT') {
|
|
|
|
|
$this->executeSetResult($action, $context);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unknown action type: ignore
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SET_RESULT action params (JSON):
|
|
|
|
|
* - testSiteID (int) OR testSiteCode (string)
|
|
|
|
|
* - value (scalar) OR valueExpr (ExpressionLanguage string)
|
|
|
|
|
*/
|
|
|
|
|
protected function executeSetResult(array $action, array $context): void
|
|
|
|
|
{
|
|
|
|
|
$paramsRaw = (string) ($action['ActionParams'] ?? '');
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
if (trim($paramsRaw) !== '') {
|
|
|
|
|
$decoded = json_decode($paramsRaw, true);
|
|
|
|
|
if (is_array($decoded)) {
|
|
|
|
|
$params = $decoded;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$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 = isset($params['testSiteID']) && is_numeric($params['testSiteID'])
|
|
|
|
|
? (int) $params['testSiteID']
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if ($testSiteID === null && !empty($params['testSiteCode'])) {
|
|
|
|
|
$testSiteCode = (string) $params['testSiteCode'];
|
|
|
|
|
$testDefSiteModel = new TestDefSiteModel();
|
|
|
|
|
$row = $testDefSiteModel->where('TestSiteCode', $testSiteCode)->where('EndDate', null)->first();
|
|
|
|
|
$testSiteID = isset($row['TestSiteID']) ? (int) $row['TestSiteID'] : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($testSiteID === null) {
|
|
|
|
|
throw new \Exception('SET_RESULT requires testSiteID or testSiteCode');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (array_key_exists('valueExpr', $params) && is_string($params['valueExpr'])) {
|
|
|
|
|
$value = $this->expr->evaluate($params['valueExpr'], $context);
|
|
|
|
|
} else {
|
|
|
|
|
$value = $params['value'] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$db = \Config\Database::connect();
|
|
|
|
|
$ok = $db->table('patres')
|
|
|
|
|
->where('OrderID', $internalOID)
|
|
|
|
|
->where('TestSiteID', $testSiteID)
|
|
|
|
|
->where('DelDate', null)
|
|
|
|
|
->update(['Result' => $value]);
|
|
|
|
|
|
|
|
|
|
if ($ok === false) {
|
|
|
|
|
throw new \Exception('SET_RESULT update failed');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|