clqms-be/app/Services/RuleEngineService.php

232 lines
7.8 KiB
PHP

<?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 {
// Check for compiled expression first
$compiled = null;
if (!empty($rule['ConditionExprCompiled'])) {
$compiled = json_decode($rule['ConditionExprCompiled'], true);
}
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);
}
}
} catch (\Throwable $e) {
log_message('error', 'Rule engine error (RuleID=' . $rid . '): ' . $e->getMessage());
continue;
}
}
}
/**
* 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');
}
}
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');
}
}
}