- Add /api/rules CRUD, nested actions, and expr validation - Add rules migration, models, and RuleEngine/Expression services - Run ORDER_CREATED rules after order create (non-blocking) and refresh tests - Update OpenAPI tags/schemas/paths and bundled docs
345 lines
12 KiB
PHP
345 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\Rule;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\Rule\RuleActionModel;
|
|
use App\Models\Rule\RuleDefModel;
|
|
use App\Models\Test\TestDefSiteModel;
|
|
use App\Services\RuleExpressionService;
|
|
use App\Traits\ResponseTrait;
|
|
|
|
class RuleController extends BaseController
|
|
{
|
|
use ResponseTrait;
|
|
|
|
protected RuleDefModel $ruleDefModel;
|
|
protected RuleActionModel $ruleActionModel;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->ruleDefModel = new RuleDefModel();
|
|
$this->ruleActionModel = new RuleActionModel();
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
try {
|
|
$eventCode = $this->request->getGet('EventCode');
|
|
$active = $this->request->getGet('Active');
|
|
$scopeType = $this->request->getGet('ScopeType');
|
|
$testSiteID = $this->request->getGet('TestSiteID');
|
|
$search = $this->request->getGet('search');
|
|
|
|
$builder = $this->ruleDefModel->where('EndDate', null);
|
|
|
|
if ($eventCode !== null && $eventCode !== '') {
|
|
$builder->where('EventCode', $eventCode);
|
|
}
|
|
if ($active !== null && $active !== '') {
|
|
$builder->where('Active', (int) $active);
|
|
}
|
|
if ($scopeType !== null && $scopeType !== '') {
|
|
$builder->where('ScopeType', $scopeType);
|
|
}
|
|
if ($testSiteID !== null && $testSiteID !== '' && is_numeric($testSiteID)) {
|
|
$builder->where('TestSiteID', (int) $testSiteID);
|
|
}
|
|
if ($search !== null && $search !== '') {
|
|
$builder->like('Name', $search);
|
|
}
|
|
|
|
$rows = $builder
|
|
->orderBy('Priority', 'ASC')
|
|
->orderBy('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');
|
|
}
|
|
|
|
$rule = $this->ruleDefModel->where('EndDate', null)->find((int) $id);
|
|
if (!$rule) {
|
|
return $this->respond([
|
|
'status' => 'failed',
|
|
'message' => 'Rule not found',
|
|
'data' => [],
|
|
], 404);
|
|
}
|
|
|
|
$actions = $this->ruleActionModel
|
|
->where('RuleID', (int) $id)
|
|
->where('EndDate', null)
|
|
->orderBy('Seq', 'ASC')
|
|
->orderBy('RuleActionID', 'ASC')
|
|
->findAll();
|
|
|
|
$rule['actions'] = $actions;
|
|
|
|
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([
|
|
'Name' => 'required|max_length[100]',
|
|
'EventCode' => 'required|max_length[50]',
|
|
'ScopeType' => 'required|in_list[GLOBAL,TESTSITE]',
|
|
'TestSiteID' => 'permit_empty|is_natural_no_zero',
|
|
'ConditionExpr' => 'permit_empty|max_length[1000]',
|
|
'Priority' => 'permit_empty|integer',
|
|
'Active' => 'permit_empty|in_list[0,1]',
|
|
]);
|
|
|
|
if (!$validation->run($input)) {
|
|
return $this->failValidationErrors($validation->getErrors());
|
|
}
|
|
|
|
if (($input['ScopeType'] ?? 'GLOBAL') === 'TESTSITE') {
|
|
if (empty($input['TestSiteID'])) {
|
|
return $this->failValidationErrors(['TestSiteID' => 'TestSiteID is required for TESTSITE scope']);
|
|
}
|
|
$testDef = new TestDefSiteModel();
|
|
$exists = $testDef->where('EndDate', null)->find((int) $input['TestSiteID']);
|
|
if (!$exists) {
|
|
return $this->failValidationErrors(['TestSiteID' => 'TestSiteID not found']);
|
|
}
|
|
} else {
|
|
$input['TestSiteID'] = null;
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$db->transStart();
|
|
|
|
try {
|
|
$ruleData = [
|
|
'Name' => $input['Name'],
|
|
'Description' => $input['Description'] ?? null,
|
|
'EventCode' => $input['EventCode'],
|
|
'ScopeType' => $input['ScopeType'],
|
|
'TestSiteID' => $input['TestSiteID'] ?? null,
|
|
'ConditionExpr' => $input['ConditionExpr'] ?? null,
|
|
'Priority' => $input['Priority'] ?? 100,
|
|
'Active' => isset($input['Active']) ? (int) $input['Active'] : 1,
|
|
];
|
|
|
|
$ruleID = $this->ruleDefModel->insert($ruleData, true);
|
|
if (!$ruleID) {
|
|
throw new \Exception('Failed to create rule');
|
|
}
|
|
|
|
if (isset($input['actions']) && is_array($input['actions'])) {
|
|
foreach ($input['actions'] as $action) {
|
|
if (!is_array($action)) {
|
|
continue;
|
|
}
|
|
|
|
$actionType = $action['ActionType'] ?? null;
|
|
if (!$actionType) {
|
|
continue;
|
|
}
|
|
|
|
$params = $action['ActionParams'] ?? null;
|
|
if (is_array($params)) {
|
|
$params = json_encode($params);
|
|
}
|
|
|
|
$this->ruleActionModel->insert([
|
|
'RuleID' => $ruleID,
|
|
'Seq' => $action['Seq'] ?? 1,
|
|
'ActionType' => $actionType,
|
|
'ActionParams' => is_string($params) ? $params : null,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$db->transComplete();
|
|
if ($db->transStatus() === false) {
|
|
throw new \Exception('Transaction failed');
|
|
}
|
|
|
|
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([
|
|
'Name' => 'permit_empty|max_length[100]',
|
|
'EventCode' => 'permit_empty|max_length[50]',
|
|
'ScopeType' => 'permit_empty|in_list[GLOBAL,TESTSITE]',
|
|
'TestSiteID' => 'permit_empty|is_natural_no_zero',
|
|
'ConditionExpr' => 'permit_empty|max_length[1000]',
|
|
'Priority' => 'permit_empty|integer',
|
|
'Active' => 'permit_empty|in_list[0,1]',
|
|
]);
|
|
|
|
if (!$validation->run($input)) {
|
|
return $this->failValidationErrors($validation->getErrors());
|
|
}
|
|
|
|
$scopeType = $input['ScopeType'] ?? $existing['ScopeType'] ?? 'GLOBAL';
|
|
$testSiteID = array_key_exists('TestSiteID', $input) ? $input['TestSiteID'] : ($existing['TestSiteID'] ?? null);
|
|
|
|
if ($scopeType === 'TESTSITE') {
|
|
if (empty($testSiteID)) {
|
|
return $this->failValidationErrors(['TestSiteID' => 'TestSiteID is required for TESTSITE scope']);
|
|
}
|
|
$testDef = new TestDefSiteModel();
|
|
$exists = $testDef->where('EndDate', null)->find((int) $testSiteID);
|
|
if (!$exists) {
|
|
return $this->failValidationErrors(['TestSiteID' => 'TestSiteID not found']);
|
|
}
|
|
} else {
|
|
$testSiteID = null;
|
|
}
|
|
|
|
try {
|
|
$updateData = [];
|
|
foreach (['Name', 'Description', 'EventCode', 'ScopeType', 'ConditionExpr', 'Priority', 'Active'] as $field) {
|
|
if (array_key_exists($field, $input)) {
|
|
$updateData[$field] = $input[$field];
|
|
}
|
|
}
|
|
$updateData['TestSiteID'] = $testSiteID;
|
|
|
|
if (!empty($updateData)) {
|
|
$this->ruleDefModel->update((int) $id, $updateData);
|
|
}
|
|
|
|
return $this->respond([
|
|
'status' => 'success',
|
|
'message' => 'Rule updated successfully',
|
|
'data' => ['RuleID' => (int) $id],
|
|
], 200);
|
|
} catch (\Throwable $e) {
|
|
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);
|
|
}
|
|
}
|
|
}
|