2026-03-16 07:24:50 +07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Controllers\Rule;
|
|
|
|
|
|
2026-03-12 06:34:56 +07:00
|
|
|
use App\Controllers\BaseController;
|
|
|
|
|
use App\Models\Rule\RuleDefModel;
|
|
|
|
|
use App\Models\Test\TestDefSiteModel;
|
|
|
|
|
use App\Services\RuleExpressionService;
|
|
|
|
|
use App\Traits\ResponseTrait;
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
class RuleController extends BaseController
|
|
|
|
|
{
|
2026-03-12 06:34:56 +07:00
|
|
|
use ResponseTrait;
|
|
|
|
|
|
|
|
|
|
protected RuleDefModel $ruleDefModel;
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->ruleDefModel = new RuleDefModel();
|
|
|
|
|
}
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
public function index()
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
$eventCode = $this->request->getGet('EventCode');
|
|
|
|
|
$testSiteID = $this->request->getGet('TestSiteID');
|
|
|
|
|
$search = $this->request->getGet('search');
|
|
|
|
|
|
|
|
|
|
$builder = $this->ruleDefModel->where('ruledef.EndDate', null);
|
|
|
|
|
|
|
|
|
|
if ($eventCode !== null && $eventCode !== '') {
|
|
|
|
|
$builder->where('ruledef.EventCode', $eventCode);
|
|
|
|
|
}
|
|
|
|
|
if ($search !== null && $search !== '') {
|
|
|
|
|
$builder->like('ruledef.RuleName', $search);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter by TestSiteID - join with mapping table
|
|
|
|
|
if ($testSiteID !== null && $testSiteID !== '' && is_numeric($testSiteID)) {
|
|
|
|
|
$builder->join('testrule', 'testrule.RuleID = ruledef.RuleID', 'inner');
|
|
|
|
|
$builder->where('testrule.TestSiteID', (int) $testSiteID);
|
|
|
|
|
$builder->where('testrule.EndDate IS NULL');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rows = $builder
|
|
|
|
|
->orderBy('ruledef.RuleID', 'ASC')
|
|
|
|
|
->findAll();
|
|
|
|
|
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'message' => 'fetch success',
|
|
|
|
|
'data' => $rows,
|
|
|
|
|
], 200);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
log_message('error', 'RuleController::index error: ' . $e->getMessage());
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'failed',
|
|
|
|
|
'message' => 'Failed to fetch rules',
|
|
|
|
|
'data' => [],
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function show($id = null)
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
if (!$id || !is_numeric($id)) {
|
|
|
|
|
return $this->failValidationErrors('RuleID is required');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 06:34:56 +07:00
|
|
|
$rule = $this->ruleDefModel->where('EndDate', null)->find((int) $id);
|
|
|
|
|
if (!$rule) {
|
2026-03-16 07:24:50 +07:00
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'failed',
|
|
|
|
|
'message' => 'Rule not found',
|
|
|
|
|
'data' => [],
|
|
|
|
|
], 404);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 16:55:03 +07:00
|
|
|
$linkedTests = $this->ruleDefModel->getLinkedTests((int) $id);
|
|
|
|
|
|
|
|
|
|
$rule['linkedTests'] = $linkedTests;
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'message' => 'fetch success',
|
|
|
|
|
'data' => $rule,
|
|
|
|
|
], 200);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
log_message('error', 'RuleController::show error: ' . $e->getMessage());
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'failed',
|
|
|
|
|
'message' => 'Failed to fetch rule',
|
|
|
|
|
'data' => [],
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function create()
|
|
|
|
|
{
|
|
|
|
|
$input = $this->request->getJSON(true) ?? [];
|
|
|
|
|
|
|
|
|
|
$validation = service('validation');
|
|
|
|
|
$validation->setRules([
|
|
|
|
|
'RuleCode' => 'required|max_length[50]',
|
|
|
|
|
'RuleName' => 'required|max_length[100]',
|
|
|
|
|
'EventCode' => 'required|max_length[50]',
|
|
|
|
|
'TestSiteIDs' => 'required',
|
|
|
|
|
'TestSiteIDs.*' => 'is_natural_no_zero',
|
|
|
|
|
'ConditionExpr' => 'permit_empty|max_length[1000]',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (!$validation->run($input)) {
|
|
|
|
|
return $this->failValidationErrors($validation->getErrors());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$testSiteIDs = $input['TestSiteIDs'] ?? [];
|
|
|
|
|
if (!is_array($testSiteIDs) || empty($testSiteIDs)) {
|
|
|
|
|
return $this->failValidationErrors(['TestSiteIDs' => 'At least one TestSiteID is required']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate all TestSiteIDs exist
|
|
|
|
|
$testDef = new TestDefSiteModel();
|
|
|
|
|
foreach ($testSiteIDs as $testSiteID) {
|
|
|
|
|
$exists = $testDef->where('EndDate', null)->find((int) $testSiteID);
|
|
|
|
|
if (!$exists) {
|
|
|
|
|
return $this->failValidationErrors(['TestSiteIDs' => "TestSiteID {$testSiteID} not found"]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$db = \Config\Database::connect();
|
|
|
|
|
$db->transStart();
|
|
|
|
|
|
2026-03-12 06:34:56 +07:00
|
|
|
try {
|
|
|
|
|
$ruleData = [
|
2026-03-12 16:55:03 +07:00
|
|
|
'RuleCode' => $input['RuleCode'],
|
|
|
|
|
'RuleName' => $input['RuleName'],
|
2026-03-12 06:34:56 +07:00
|
|
|
'Description' => $input['Description'] ?? null,
|
|
|
|
|
'EventCode' => $input['EventCode'],
|
|
|
|
|
'ConditionExpr' => $input['ConditionExpr'] ?? null,
|
2026-03-12 16:55:03 +07:00
|
|
|
'ConditionExprCompiled' => $input['ConditionExprCompiled'] ?? null,
|
2026-03-12 06:34:56 +07:00
|
|
|
];
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
$ruleID = $this->ruleDefModel->insert($ruleData, true);
|
|
|
|
|
if (!$ruleID) {
|
|
|
|
|
throw new \Exception('Failed to create rule');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 16:55:03 +07:00
|
|
|
// Link rule to test sites
|
|
|
|
|
foreach ($testSiteIDs as $testSiteID) {
|
|
|
|
|
$this->ruleDefModel->linkTest($ruleID, (int) $testSiteID);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 06:34:56 +07:00
|
|
|
$db->transComplete();
|
|
|
|
|
if ($db->transStatus() === false) {
|
|
|
|
|
throw new \Exception('Transaction failed');
|
|
|
|
|
}
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
return $this->respondCreated([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'message' => 'Rule created successfully',
|
|
|
|
|
'data' => ['RuleID' => $ruleID],
|
|
|
|
|
], 201);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$db->transRollback();
|
|
|
|
|
log_message('error', 'RuleController::create error: ' . $e->getMessage());
|
|
|
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function update($id = null)
|
|
|
|
|
{
|
|
|
|
|
$input = $this->request->getJSON(true) ?? [];
|
|
|
|
|
|
|
|
|
|
if (!$id || !is_numeric($id)) {
|
|
|
|
|
$id = $input['RuleID'] ?? null;
|
|
|
|
|
}
|
|
|
|
|
if (!$id || !is_numeric($id)) {
|
|
|
|
|
return $this->failValidationErrors('RuleID is required');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$existing = $this->ruleDefModel->where('EndDate', null)->find((int) $id);
|
|
|
|
|
if (!$existing) {
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'failed',
|
|
|
|
|
'message' => 'Rule not found',
|
|
|
|
|
'data' => [],
|
|
|
|
|
], 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$validation = service('validation');
|
|
|
|
|
$validation->setRules([
|
|
|
|
|
'RuleCode' => 'permit_empty|max_length[50]',
|
|
|
|
|
'RuleName' => 'permit_empty|max_length[100]',
|
|
|
|
|
'EventCode' => 'permit_empty|max_length[50]',
|
|
|
|
|
'TestSiteIDs' => 'permit_empty',
|
|
|
|
|
'TestSiteIDs.*' => 'is_natural_no_zero',
|
|
|
|
|
'ConditionExpr' => 'permit_empty|max_length[1000]',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (!$validation->run($input)) {
|
|
|
|
|
return $this->failValidationErrors($validation->getErrors());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$db = \Config\Database::connect();
|
|
|
|
|
$db->transStart();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$updateData = [];
|
|
|
|
|
foreach (['RuleCode', 'RuleName', 'Description', 'EventCode', 'ConditionExpr', 'ConditionExprCompiled'] as $field) {
|
|
|
|
|
if (array_key_exists($field, $input)) {
|
|
|
|
|
$updateData[$field] = $input[$field];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($updateData)) {
|
|
|
|
|
$this->ruleDefModel->update((int) $id, $updateData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update test site mappings if provided
|
|
|
|
|
if (isset($input['TestSiteIDs']) && is_array($input['TestSiteIDs'])) {
|
|
|
|
|
$testSiteIDs = $input['TestSiteIDs'];
|
|
|
|
|
|
|
|
|
|
// Validate all TestSiteIDs exist
|
|
|
|
|
$testDef = new TestDefSiteModel();
|
|
|
|
|
foreach ($testSiteIDs as $testSiteID) {
|
|
|
|
|
$exists = $testDef->where('EndDate', null)->find((int) $testSiteID);
|
|
|
|
|
if (!$exists) {
|
|
|
|
|
throw new \Exception("TestSiteID {$testSiteID} not found");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current linked tests
|
|
|
|
|
$currentLinks = $this->ruleDefModel->getLinkedTests((int) $id);
|
|
|
|
|
|
|
|
|
|
// Unlink tests that are no longer in the list
|
|
|
|
|
foreach ($currentLinks as $currentTestSiteID) {
|
|
|
|
|
if (!in_array($currentTestSiteID, $testSiteIDs)) {
|
|
|
|
|
$this->ruleDefModel->unlinkTest((int) $id, $currentTestSiteID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Link new tests
|
|
|
|
|
foreach ($testSiteIDs as $testSiteID) {
|
|
|
|
|
if (!in_array($testSiteID, $currentLinks)) {
|
|
|
|
|
$this->ruleDefModel->linkTest((int) $id, (int) $testSiteID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$db->transComplete();
|
|
|
|
|
if ($db->transStatus() === false) {
|
|
|
|
|
throw new \Exception('Transaction failed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'message' => 'Rule updated successfully',
|
|
|
|
|
'data' => ['RuleID' => (int) $id],
|
|
|
|
|
], 200);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$db->transRollback();
|
|
|
|
|
log_message('error', 'RuleController::update error: ' . $e->getMessage());
|
|
|
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function delete($id = null)
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
if (!$id || !is_numeric($id)) {
|
|
|
|
|
return $this->failValidationErrors('RuleID is required');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$existing = $this->ruleDefModel->where('EndDate', null)->find((int) $id);
|
|
|
|
|
if (!$existing) {
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'failed',
|
|
|
|
|
'message' => 'Rule not found',
|
|
|
|
|
'data' => [],
|
|
|
|
|
], 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->ruleDefModel->delete((int) $id);
|
|
|
|
|
|
|
|
|
|
return $this->respondDeleted([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'message' => 'Rule deleted successfully',
|
|
|
|
|
'data' => ['RuleID' => (int) $id],
|
|
|
|
|
]);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
log_message('error', 'RuleController::delete error: ' . $e->getMessage());
|
|
|
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function validateExpr()
|
|
|
|
|
{
|
|
|
|
|
$input = $this->request->getJSON(true) ?? [];
|
|
|
|
|
$expr = $input['expr'] ?? '';
|
|
|
|
|
$context = $input['context'] ?? [];
|
|
|
|
|
|
|
|
|
|
if (!is_string($expr) || trim($expr) === '') {
|
|
|
|
|
return $this->failValidationErrors(['expr' => 'expr is required']);
|
|
|
|
|
}
|
|
|
|
|
if (!is_array($context)) {
|
|
|
|
|
return $this->failValidationErrors(['context' => 'context must be an object']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$svc = new RuleExpressionService();
|
|
|
|
|
$result = $svc->evaluate($expr, $context);
|
|
|
|
|
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'data' => [
|
|
|
|
|
'valid' => true,
|
|
|
|
|
'result' => $result,
|
|
|
|
|
],
|
|
|
|
|
], 200);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'failed',
|
|
|
|
|
'data' => [
|
|
|
|
|
'valid' => false,
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
],
|
|
|
|
|
], 200);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compile DSL expression to engine-compatible structure.
|
|
|
|
|
* Frontend calls this when user clicks "Compile" button.
|
|
|
|
|
*/
|
|
|
|
|
public function compile()
|
|
|
|
|
{
|
|
|
|
|
$input = $this->request->getJSON(true) ?? [];
|
|
|
|
|
$expr = $input['expr'] ?? '';
|
|
|
|
|
|
|
|
|
|
if (!is_string($expr) || trim($expr) === '') {
|
|
|
|
|
return $this->failValidationErrors(['expr' => 'Expression is required']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$svc = new RuleExpressionService();
|
|
|
|
|
$compiled = $svc->compile($expr);
|
|
|
|
|
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'data' => [
|
|
|
|
|
'raw' => $expr,
|
|
|
|
|
'compiled' => $compiled,
|
|
|
|
|
'conditionExprCompiled' => json_encode($compiled),
|
|
|
|
|
],
|
|
|
|
|
], 200);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
return $this->respond([
|
|
|
|
|
'status' => 'failed',
|
|
|
|
|
'message' => 'Compilation failed',
|
|
|
|
|
'data' => [
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
],
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|