feat: add rules engine API and order-created hook
- 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
This commit is contained in:
parent
911846592f
commit
88be3f3809
@ -346,6 +346,21 @@ $routes->group('api', function ($routes) {
|
||||
$routes->post('status', 'OrderTestController::updateStatus');
|
||||
});
|
||||
|
||||
// Rules
|
||||
$routes->group('rules', function ($routes) {
|
||||
$routes->get('/', 'Rule\RuleController::index');
|
||||
$routes->get('(:num)', 'Rule\RuleController::show/$1');
|
||||
$routes->post('/', 'Rule\RuleController::create');
|
||||
$routes->patch('(:num)', 'Rule\RuleController::update/$1');
|
||||
$routes->delete('(:num)', 'Rule\RuleController::delete/$1');
|
||||
$routes->post('validate', 'Rule\RuleController::validateExpr');
|
||||
|
||||
$routes->get('(:num)/actions', 'Rule\RuleActionController::index/$1');
|
||||
$routes->post('(:num)/actions', 'Rule\RuleActionController::create/$1');
|
||||
$routes->patch('(:num)/actions/(:num)', 'Rule\RuleActionController::update/$1/$2');
|
||||
$routes->delete('(:num)/actions/(:num)', 'Rule\RuleActionController::delete/$1/$2');
|
||||
});
|
||||
|
||||
// Demo/Test Routes (No Auth)
|
||||
$routes->group('api/demo', function ($routes) {
|
||||
$routes->post('order', 'Test\DemoOrderController::createDemoOrder');
|
||||
|
||||
@ -7,6 +7,7 @@ use App\Libraries\ValueSet;
|
||||
use App\Models\OrderTest\OrderTestModel;
|
||||
use App\Models\Patient\PatientModel;
|
||||
use App\Models\PatVisit\PatVisitModel;
|
||||
use App\Services\RuleEngineService;
|
||||
|
||||
class OrderTestController extends Controller {
|
||||
use ResponseTrait;
|
||||
@ -153,6 +154,21 @@ class OrderTestController extends Controller {
|
||||
$order['Specimens'] = $this->getOrderSpecimens($order['InternalOID']);
|
||||
$order['Tests'] = $this->getOrderTests($order['InternalOID']);
|
||||
|
||||
// Run common rules for ORDER_CREATED (non-blocking)
|
||||
try {
|
||||
$ruleEngine = new RuleEngineService();
|
||||
$ruleEngine->run('ORDER_CREATED', [
|
||||
'order' => $order,
|
||||
'tests' => $order['Tests'],
|
||||
'input' => $input,
|
||||
]);
|
||||
|
||||
// Refresh tests in case rules updated results
|
||||
$order['Tests'] = $this->getOrderTests($order['InternalOID']);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'OrderTestController::create rule engine error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => 'Order created successfully',
|
||||
|
||||
231
app/Controllers/Rule/RuleActionController.php
Normal file
231
app/Controllers/Rule/RuleActionController.php
Normal file
@ -0,0 +1,231 @@
|
||||
<?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\Traits\ResponseTrait;
|
||||
|
||||
class RuleActionController extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected RuleDefModel $ruleDefModel;
|
||||
protected RuleActionModel $ruleActionModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ruleDefModel = new RuleDefModel();
|
||||
$this->ruleActionModel = new RuleActionModel();
|
||||
}
|
||||
|
||||
public function index($ruleID = null)
|
||||
{
|
||||
try {
|
||||
if (!$ruleID || !is_numeric($ruleID)) {
|
||||
return $this->failValidationErrors('RuleID is required');
|
||||
}
|
||||
|
||||
$rule = $this->ruleDefModel->where('EndDate', null)->find((int) $ruleID);
|
||||
if (!$rule) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Rule not found',
|
||||
'data' => [],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$rows = $this->ruleActionModel
|
||||
->where('RuleID', (int) $ruleID)
|
||||
->where('EndDate', null)
|
||||
->orderBy('Seq', 'ASC')
|
||||
->orderBy('RuleActionID', 'ASC')
|
||||
->findAll();
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows,
|
||||
], 200);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'RuleActionController::index error: ' . $e->getMessage());
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create($ruleID = null)
|
||||
{
|
||||
$input = $this->request->getJSON(true) ?? [];
|
||||
|
||||
if (!$ruleID || !is_numeric($ruleID)) {
|
||||
$ruleID = $input['RuleID'] ?? null;
|
||||
}
|
||||
if (!$ruleID || !is_numeric($ruleID)) {
|
||||
return $this->failValidationErrors('RuleID is required');
|
||||
}
|
||||
|
||||
$rule = $this->ruleDefModel->where('EndDate', null)->find((int) $ruleID);
|
||||
if (!$rule) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Rule not found',
|
||||
'data' => [],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validation = service('validation');
|
||||
$validation->setRules([
|
||||
'Seq' => 'permit_empty|integer',
|
||||
'ActionType' => 'required|max_length[50]',
|
||||
]);
|
||||
|
||||
if (!$validation->run($input)) {
|
||||
return $this->failValidationErrors($validation->getErrors());
|
||||
}
|
||||
|
||||
// Light validation for SET_RESULT params
|
||||
$actionType = strtoupper((string) $input['ActionType']);
|
||||
$params = $input['ActionParams'] ?? null;
|
||||
if ($actionType === 'SET_RESULT') {
|
||||
$decoded = is_array($params) ? $params : (is_string($params) ? json_decode($params, true) : null);
|
||||
if (!is_array($decoded)) {
|
||||
return $this->failValidationErrors(['ActionParams' => 'ActionParams must be JSON object for SET_RESULT']);
|
||||
}
|
||||
|
||||
if (empty($decoded['testSiteID']) && empty($decoded['testSiteCode'])) {
|
||||
return $this->failValidationErrors(['ActionParams' => 'SET_RESULT requires testSiteID or testSiteCode']);
|
||||
}
|
||||
if (!array_key_exists('value', $decoded) && !array_key_exists('valueExpr', $decoded)) {
|
||||
return $this->failValidationErrors(['ActionParams' => 'SET_RESULT requires value or valueExpr']);
|
||||
}
|
||||
|
||||
if (!empty($decoded['testSiteID']) && is_numeric($decoded['testSiteID'])) {
|
||||
$testDef = new TestDefSiteModel();
|
||||
$exists = $testDef->where('EndDate', null)->find((int) $decoded['testSiteID']);
|
||||
if (!$exists) {
|
||||
return $this->failValidationErrors(['ActionParams' => 'testSiteID not found']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (is_array($params)) {
|
||||
$params = json_encode($params);
|
||||
}
|
||||
|
||||
$id = $this->ruleActionModel->insert([
|
||||
'RuleID' => (int) $ruleID,
|
||||
'Seq' => $input['Seq'] ?? 1,
|
||||
'ActionType' => $input['ActionType'],
|
||||
'ActionParams' => is_string($params) ? $params : null,
|
||||
], true);
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => 'Action created successfully',
|
||||
'data' => ['RuleActionID' => $id],
|
||||
], 201);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'RuleActionController::create error: ' . $e->getMessage());
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($ruleID = null, $actionID = null)
|
||||
{
|
||||
$input = $this->request->getJSON(true) ?? [];
|
||||
|
||||
if (!$ruleID || !is_numeric($ruleID)) {
|
||||
$ruleID = $input['RuleID'] ?? null;
|
||||
}
|
||||
if (!$actionID || !is_numeric($actionID)) {
|
||||
$actionID = $input['RuleActionID'] ?? null;
|
||||
}
|
||||
if (!$ruleID || !is_numeric($ruleID) || !$actionID || !is_numeric($actionID)) {
|
||||
return $this->failValidationErrors('RuleID and RuleActionID are required');
|
||||
}
|
||||
|
||||
$rule = $this->ruleDefModel->where('EndDate', null)->find((int) $ruleID);
|
||||
if (!$rule) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Rule not found',
|
||||
'data' => [],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$existing = $this->ruleActionModel->where('EndDate', null)->find((int) $actionID);
|
||||
if (!$existing || (int) ($existing['RuleID'] ?? 0) !== (int) $ruleID) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Action not found',
|
||||
'data' => [],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validation = service('validation');
|
||||
$validation->setRules([
|
||||
'Seq' => 'permit_empty|integer',
|
||||
'ActionType' => 'permit_empty|max_length[50]',
|
||||
]);
|
||||
if (!$validation->run($input)) {
|
||||
return $this->failValidationErrors($validation->getErrors());
|
||||
}
|
||||
|
||||
try {
|
||||
$updateData = [];
|
||||
foreach (['Seq', 'ActionType', 'ActionParams'] as $field) {
|
||||
if (array_key_exists($field, $input)) {
|
||||
$updateData[$field] = $input[$field];
|
||||
}
|
||||
}
|
||||
if (isset($updateData['ActionParams']) && is_array($updateData['ActionParams'])) {
|
||||
$updateData['ActionParams'] = json_encode($updateData['ActionParams']);
|
||||
}
|
||||
|
||||
if (!empty($updateData)) {
|
||||
$this->ruleActionModel->update((int) $actionID, $updateData);
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Action updated successfully',
|
||||
'data' => ['RuleActionID' => (int) $actionID],
|
||||
], 200);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'RuleActionController::update error: ' . $e->getMessage());
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($ruleID = null, $actionID = null)
|
||||
{
|
||||
try {
|
||||
if (!$ruleID || !is_numeric($ruleID) || !$actionID || !is_numeric($actionID)) {
|
||||
return $this->failValidationErrors('RuleID and RuleActionID are required');
|
||||
}
|
||||
|
||||
$existing = $this->ruleActionModel->where('EndDate', null)->find((int) $actionID);
|
||||
if (!$existing || (int) ($existing['RuleID'] ?? 0) !== (int) $ruleID) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Action not found',
|
||||
'data' => [],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$this->ruleActionModel->delete((int) $actionID);
|
||||
|
||||
return $this->respondDeleted([
|
||||
'status' => 'success',
|
||||
'message' => 'Action deleted successfully',
|
||||
'data' => ['RuleActionID' => (int) $actionID],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'RuleActionController::delete error: ' . $e->getMessage());
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
344
app/Controllers/Rule/RuleController.php
Normal file
344
app/Controllers/Rule/RuleController.php
Normal file
@ -0,0 +1,344 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/Database/Migrations/2026-03-11-000030_CreateRules.php
Normal file
57
app/Database/Migrations/2026-03-11-000030_CreateRules.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateRules extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
// ruledef
|
||||
$this->forge->addField([
|
||||
'RuleID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'Name' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => false],
|
||||
'Description' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'EventCode' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => false],
|
||||
'ScopeType' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => false, 'default' => 'GLOBAL'],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'ConditionExpr' => ['type' => 'VARCHAR', 'constraint' => 1000, 'null' => true],
|
||||
'Priority' => ['type' => 'INT', 'null' => true, 'default' => 100],
|
||||
'Active' => ['type' => 'TINYINT', 'constraint' => 1, 'null' => true, 'default' => 1],
|
||||
'CreateDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'StartDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'EndDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('RuleID', true);
|
||||
$this->forge->addKey('EventCode');
|
||||
$this->forge->addKey('ScopeType');
|
||||
$this->forge->addKey('TestSiteID');
|
||||
$this->forge->createTable('ruledef');
|
||||
|
||||
// Optional scope FK (only when ScopeType=TESTSITE)
|
||||
$this->db->query('ALTER TABLE `ruledef` ADD CONSTRAINT `fk_ruledef_testsite` FOREIGN KEY (`TestSiteID`) REFERENCES `testdefsite`(`TestSiteID`) ON DELETE CASCADE ON UPDATE CASCADE');
|
||||
|
||||
// ruleaction
|
||||
$this->forge->addField([
|
||||
'RuleActionID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'RuleID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'Seq' => ['type' => 'INT', 'null' => true, 'default' => 1],
|
||||
'ActionType' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => false],
|
||||
'ActionParams' => ['type' => 'TEXT', 'null' => true],
|
||||
'CreateDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'EndDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('RuleActionID', true);
|
||||
$this->forge->addKey('RuleID');
|
||||
$this->forge->createTable('ruleaction');
|
||||
|
||||
$this->db->query('ALTER TABLE `ruleaction` ADD CONSTRAINT `fk_ruleaction_ruledef` FOREIGN KEY (`RuleID`) REFERENCES `ruledef`(`RuleID`) ON DELETE CASCADE ON UPDATE CASCADE');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('ruleaction');
|
||||
$this->forge->dropTable('ruledef');
|
||||
}
|
||||
}
|
||||
40
app/Models/Rule/RuleActionModel.php
Normal file
40
app/Models/Rule/RuleActionModel.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Rule;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class RuleActionModel extends BaseModel
|
||||
{
|
||||
protected $table = 'ruleaction';
|
||||
protected $primaryKey = 'RuleActionID';
|
||||
protected $allowedFields = [
|
||||
'RuleID',
|
||||
'Seq',
|
||||
'ActionType',
|
||||
'ActionParams',
|
||||
'CreateDate',
|
||||
'EndDate',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = 'EndDate';
|
||||
|
||||
public function getActiveByRuleIDs(array $ruleIDs): array
|
||||
{
|
||||
if (empty($ruleIDs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->whereIn('RuleID', $ruleIDs)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('RuleID', 'ASC')
|
||||
->orderBy('Seq', 'ASC')
|
||||
->orderBy('RuleActionID', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
61
app/Models/Rule/RuleDefModel.php
Normal file
61
app/Models/Rule/RuleDefModel.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Rule;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class RuleDefModel extends BaseModel
|
||||
{
|
||||
protected $table = 'ruledef';
|
||||
protected $primaryKey = 'RuleID';
|
||||
protected $allowedFields = [
|
||||
'Name',
|
||||
'Description',
|
||||
'EventCode',
|
||||
'ScopeType',
|
||||
'TestSiteID',
|
||||
'ConditionExpr',
|
||||
'Priority',
|
||||
'Active',
|
||||
'CreateDate',
|
||||
'StartDate',
|
||||
'EndDate',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = 'StartDate';
|
||||
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = 'EndDate';
|
||||
|
||||
/**
|
||||
* Fetch active rules for an event, optionally scoped.
|
||||
*
|
||||
* Scope behavior:
|
||||
* - Always returns GLOBAL rules
|
||||
* - If $testSiteID provided, also returns TESTSITE rules matching TestSiteID
|
||||
*/
|
||||
public function getActiveByEvent(string $eventCode, ?int $testSiteID = null): array
|
||||
{
|
||||
$builder = $this->where('EventCode', $eventCode)
|
||||
->where('EndDate IS NULL')
|
||||
->where('Active', 1)
|
||||
->groupStart()
|
||||
->where('ScopeType', 'GLOBAL');
|
||||
|
||||
if ($testSiteID !== null) {
|
||||
$builder->orGroupStart()
|
||||
->where('ScopeType', 'TESTSITE')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$builder->groupEnd();
|
||||
|
||||
return $builder
|
||||
->orderBy('Priority', 'ASC')
|
||||
->orderBy('RuleID', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
145
app/Services/RuleEngineService.php
Normal file
145
app/Services/RuleEngineService.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?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 {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
46
app/Services/RuleExpressionService.php
Normal file
46
app/Services/RuleExpressionService.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
use Symfony\Component\ExpressionLanguage\ParsedExpression;
|
||||
|
||||
class RuleExpressionService
|
||||
{
|
||||
protected ExpressionLanguage $language;
|
||||
|
||||
/** @var array<string, ParsedExpression> */
|
||||
protected array $parsedCache = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->language = new ExpressionLanguage();
|
||||
}
|
||||
|
||||
public function evaluate(string $expr, array $context = [])
|
||||
{
|
||||
$expr = trim($expr);
|
||||
if ($expr === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$names = array_keys($context);
|
||||
sort($names);
|
||||
$cacheKey = md5($expr . '|' . implode(',', $names));
|
||||
|
||||
if (!isset($this->parsedCache[$cacheKey])) {
|
||||
$this->parsedCache[$cacheKey] = $this->language->parse($expr, $names);
|
||||
}
|
||||
|
||||
return $this->language->evaluate($this->parsedCache[$cacheKey], $context);
|
||||
}
|
||||
|
||||
public function evaluateBoolean(?string $expr, array $context = []): bool
|
||||
{
|
||||
if ($expr === null || trim($expr) === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) $this->evaluate($expr, $context);
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,8 @@
|
||||
"php": "^8.1",
|
||||
"codeigniter4/framework": "^4.0",
|
||||
"firebase/php-jwt": "^6.11",
|
||||
"mossadal/math-parser": "^1.3"
|
||||
"mossadal/math-parser": "^1.3",
|
||||
"symfony/expression-language": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.24",
|
||||
|
||||
709
composer.lock
generated
709
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "8fffd5cbb5e940a076c93e72a52f7734",
|
||||
"content-hash": "b111968eaeab80698adb5ca0eaeeb8c1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "codeigniter4/framework",
|
||||
@ -255,6 +255,108 @@
|
||||
},
|
||||
"time": "2018-09-15T22:20:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/cache.git",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Cache\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for caching libraries",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"psr",
|
||||
"psr-6"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/cache/tree/3.0.0"
|
||||
},
|
||||
"time": "2021-02-03T23:26:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
"version": "2.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/container.git",
|
||||
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Container\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||
"homepage": "https://github.com/php-fig/container",
|
||||
"keywords": [
|
||||
"PSR-11",
|
||||
"container",
|
||||
"container-interface",
|
||||
"container-interop",
|
||||
"psr"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/container/issues",
|
||||
"source": "https://github.com/php-fig/container/tree/2.0.2"
|
||||
},
|
||||
"time": "2021-11-05T16:47:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.2",
|
||||
@ -304,6 +406,489 @@
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/cache",
|
||||
"version": "v7.4.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/cache.git",
|
||||
"reference": "665522ec357540e66c294c08583b40ee576574f0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/cache/zipball/665522ec357540e66c294c08583b40ee576574f0",
|
||||
"reference": "665522ec357540e66c294c08583b40ee576574f0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"psr/cache": "^2.0|^3.0",
|
||||
"psr/log": "^1.1|^2|^3",
|
||||
"symfony/cache-contracts": "^3.6",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/var-exporter": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<3.6",
|
||||
"ext-redis": "<6.1",
|
||||
"ext-relay": "<0.12.1",
|
||||
"symfony/dependency-injection": "<6.4",
|
||||
"symfony/http-kernel": "<6.4",
|
||||
"symfony/var-dumper": "<6.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/cache-implementation": "2.0|3.0",
|
||||
"psr/simple-cache-implementation": "1.0|2.0|3.0",
|
||||
"symfony/cache-implementation": "1.1|2.0|3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cache/integration-tests": "dev-master",
|
||||
"doctrine/dbal": "^3.6|^4",
|
||||
"predis/predis": "^1.1|^2.0",
|
||||
"psr/simple-cache": "^1.0|^2.0|^3.0",
|
||||
"symfony/clock": "^6.4|^7.0|^8.0",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/filesystem": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Cache\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Traits/ValueWrapper.php"
|
||||
],
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides extended PSR-6, PSR-16 (and tags) implementations",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"caching",
|
||||
"psr6"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/cache/tree/v7.4.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-06T08:14:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/cache-contracts",
|
||||
"version": "v3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/cache-contracts.git",
|
||||
"reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868",
|
||||
"reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/cache": "^3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Contracts\\Cache\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Generic abstractions related to caching",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"abstractions",
|
||||
"contracts",
|
||||
"decoupling",
|
||||
"interfaces",
|
||||
"interoperability",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/cache-contracts/tree/v3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-13T15:25:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/expression-language",
|
||||
"version": "v7.4.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/expression-language.git",
|
||||
"reference": "f3a6497eb6573e185f2ec41cd3b3f0cd68ddf667"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/expression-language/zipball/f3a6497eb6573e185f2ec41cd3b3f0cd68ddf667",
|
||||
"reference": "f3a6497eb6573e185f2ec41cd3b3f0cd68ddf667",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/cache": "^6.4|^7.0|^8.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/service-contracts": "^2.5|^3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\ExpressionLanguage\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides an engine that can compile and evaluate expressions",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/expression-language/tree/v7.4.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-05T08:47:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
"version": "v3.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/service-contracts.git",
|
||||
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/container": "^1.1|^2.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-psr": "<1.1|>=2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Contracts\\Service\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Test/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Generic abstractions related to writing services",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"abstractions",
|
||||
"contracts",
|
||||
"decoupling",
|
||||
"interfaces",
|
||||
"interoperability",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-15T11:30:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-exporter",
|
||||
"version": "v7.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-exporter.git",
|
||||
"reference": "03a60f169c79a28513a78c967316fbc8bf17816f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f",
|
||||
"reference": "03a60f169c79a28513a78c967316fbc8bf17816f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||
"symfony/serializer": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\VarExporter\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Allows exporting any serializable PHP data structure to plain PHP code",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"clone",
|
||||
"construct",
|
||||
"export",
|
||||
"hydrate",
|
||||
"instantiate",
|
||||
"lazy-loading",
|
||||
"proxy",
|
||||
"serialize"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-exporter/tree/v7.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-11T10:15:23+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
@ -1088,59 +1673,6 @@
|
||||
],
|
||||
"time": "2025-06-20T11:29:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
"version": "2.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/container.git",
|
||||
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Container\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||
"homepage": "https://github.com/php-fig/container",
|
||||
"keywords": [
|
||||
"PSR-11",
|
||||
"container",
|
||||
"container-interface",
|
||||
"container-interop",
|
||||
"psr"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/container/issues",
|
||||
"source": "https://github.com/php-fig/container/tree/2.0.2"
|
||||
},
|
||||
"time": "2021-11-05T16:47:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
"version": "2.0.1",
|
||||
@ -2057,73 +2589,6 @@
|
||||
],
|
||||
"time": "2023-02-07T11:34:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
"version": "1.2.3",
|
||||
@ -2184,5 +2649,5 @@
|
||||
"php": "^8.1"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@ -53,6 +53,8 @@ tags:
|
||||
description: Laboratory equipment and instrument management
|
||||
- name: Users
|
||||
description: User management and administration
|
||||
- name: Rules
|
||||
description: Common rule engine (events, conditions, actions)
|
||||
paths:
|
||||
/api/auth/login:
|
||||
post:
|
||||
@ -3056,6 +3058,354 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/api/rules:
|
||||
get:
|
||||
tags:
|
||||
- Rules
|
||||
summary: List rules
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: EventCode
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: Filter by event code
|
||||
- name: Active
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
description: Filter by active flag
|
||||
- name: ScopeType
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- GLOBAL
|
||||
- TESTSITE
|
||||
description: Filter by scope type
|
||||
- name: TestSiteID
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: Filter by TestSiteID (for TESTSITE scope)
|
||||
- name: search
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: Search by rule name
|
||||
responses:
|
||||
'200':
|
||||
description: List of rules
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RuleDef'
|
||||
post:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Create rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Name:
|
||||
type: string
|
||||
Description:
|
||||
type: string
|
||||
EventCode:
|
||||
type: string
|
||||
example: ORDER_CREATED
|
||||
ScopeType:
|
||||
type: string
|
||||
enum:
|
||||
- GLOBAL
|
||||
- TESTSITE
|
||||
TestSiteID:
|
||||
type: integer
|
||||
nullable: true
|
||||
ConditionExpr:
|
||||
type: string
|
||||
nullable: true
|
||||
Priority:
|
||||
type: integer
|
||||
Active:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
Seq:
|
||||
type: integer
|
||||
ActionType:
|
||||
type: string
|
||||
ActionParams:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: object
|
||||
required:
|
||||
- Name
|
||||
- EventCode
|
||||
- ScopeType
|
||||
responses:
|
||||
'201':
|
||||
description: Rule created
|
||||
/api/rules/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Get rule (with actions)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Rule details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/RuleWithActions'
|
||||
'404':
|
||||
description: Rule not found
|
||||
patch:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Update rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Name:
|
||||
type: string
|
||||
Description:
|
||||
type: string
|
||||
EventCode:
|
||||
type: string
|
||||
ScopeType:
|
||||
type: string
|
||||
enum:
|
||||
- GLOBAL
|
||||
- TESTSITE
|
||||
TestSiteID:
|
||||
type: integer
|
||||
nullable: true
|
||||
ConditionExpr:
|
||||
type: string
|
||||
nullable: true
|
||||
Priority:
|
||||
type: integer
|
||||
Active:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
responses:
|
||||
'200':
|
||||
description: Rule updated
|
||||
'404':
|
||||
description: Rule not found
|
||||
delete:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Soft delete rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Rule deleted
|
||||
'404':
|
||||
description: Rule not found
|
||||
/api/rules/validate:
|
||||
post:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Validate/evaluate an expression
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
expr:
|
||||
type: string
|
||||
context:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- expr
|
||||
responses:
|
||||
'200':
|
||||
description: Validation result
|
||||
/api/rules/{id}/actions:
|
||||
get:
|
||||
tags:
|
||||
- Rules
|
||||
summary: List actions for a rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Actions list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RuleAction'
|
||||
post:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Create action for a rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Seq:
|
||||
type: integer
|
||||
ActionType:
|
||||
type: string
|
||||
ActionParams:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: object
|
||||
required:
|
||||
- ActionType
|
||||
responses:
|
||||
'201':
|
||||
description: Action created
|
||||
/api/rules/{id}/actions/{actionId}:
|
||||
patch:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Update action
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: actionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Seq:
|
||||
type: integer
|
||||
ActionType:
|
||||
type: string
|
||||
ActionParams:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: object
|
||||
responses:
|
||||
'200':
|
||||
description: Action updated
|
||||
delete:
|
||||
tags:
|
||||
- Rules
|
||||
summary: Soft delete action
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: actionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Action deleted
|
||||
/api/specimen:
|
||||
get:
|
||||
tags:
|
||||
@ -6716,6 +7066,81 @@ components:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
RuleDef:
|
||||
type: object
|
||||
properties:
|
||||
RuleID:
|
||||
type: integer
|
||||
Name:
|
||||
type: string
|
||||
Description:
|
||||
type: string
|
||||
nullable: true
|
||||
EventCode:
|
||||
type: string
|
||||
ScopeType:
|
||||
type: string
|
||||
enum:
|
||||
- GLOBAL
|
||||
- TESTSITE
|
||||
TestSiteID:
|
||||
type: integer
|
||||
nullable: true
|
||||
ConditionExpr:
|
||||
type: string
|
||||
nullable: true
|
||||
Priority:
|
||||
type: integer
|
||||
Active:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
CreateDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
StartDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
EndDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
RuleAction:
|
||||
type: object
|
||||
properties:
|
||||
RuleActionID:
|
||||
type: integer
|
||||
RuleID:
|
||||
type: integer
|
||||
Seq:
|
||||
type: integer
|
||||
ActionType:
|
||||
type: string
|
||||
example: SET_RESULT
|
||||
ActionParams:
|
||||
type: string
|
||||
description: JSON string parameters
|
||||
nullable: true
|
||||
CreateDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
EndDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
RuleWithActions:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuleDef'
|
||||
- type: object
|
||||
properties:
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RuleAction'
|
||||
Contact:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@ -55,6 +55,8 @@ tags:
|
||||
description: Laboratory equipment and instrument management
|
||||
- name: Users
|
||||
description: User management and administration
|
||||
- name: Rules
|
||||
description: Common rule engine (events, conditions, actions)
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
@ -182,6 +184,14 @@ components:
|
||||
UserListResponse:
|
||||
$ref: './components/schemas/user.yaml#/UserListResponse'
|
||||
|
||||
# Rules schemas
|
||||
RuleDef:
|
||||
$ref: './components/schemas/rules.yaml#/RuleDef'
|
||||
RuleAction:
|
||||
$ref: './components/schemas/rules.yaml#/RuleAction'
|
||||
RuleWithActions:
|
||||
$ref: './components/schemas/rules.yaml#/RuleWithActions'
|
||||
|
||||
# Paths are in separate files in the paths/ directory
|
||||
# To view the complete API with all paths, use: api-docs.bundled.yaml
|
||||
# To rebuild the bundle after changes: python bundle-api-docs.py
|
||||
|
||||
73
public/components/schemas/rules.yaml
Normal file
73
public/components/schemas/rules.yaml
Normal file
@ -0,0 +1,73 @@
|
||||
RuleDef:
|
||||
type: object
|
||||
properties:
|
||||
RuleID:
|
||||
type: integer
|
||||
Name:
|
||||
type: string
|
||||
Description:
|
||||
type: string
|
||||
nullable: true
|
||||
EventCode:
|
||||
type: string
|
||||
ScopeType:
|
||||
type: string
|
||||
enum: [GLOBAL, TESTSITE]
|
||||
TestSiteID:
|
||||
type: integer
|
||||
nullable: true
|
||||
ConditionExpr:
|
||||
type: string
|
||||
nullable: true
|
||||
Priority:
|
||||
type: integer
|
||||
Active:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
CreateDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
StartDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
EndDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
|
||||
RuleAction:
|
||||
type: object
|
||||
properties:
|
||||
RuleActionID:
|
||||
type: integer
|
||||
RuleID:
|
||||
type: integer
|
||||
Seq:
|
||||
type: integer
|
||||
ActionType:
|
||||
type: string
|
||||
example: SET_RESULT
|
||||
ActionParams:
|
||||
type: string
|
||||
description: JSON string parameters
|
||||
nullable: true
|
||||
CreateDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
EndDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
|
||||
RuleWithActions:
|
||||
allOf:
|
||||
- $ref: './rules.yaml#/RuleDef'
|
||||
- type: object
|
||||
properties:
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
$ref: './rules.yaml#/RuleAction'
|
||||
316
public/paths/rules.yaml
Normal file
316
public/paths/rules.yaml
Normal file
@ -0,0 +1,316 @@
|
||||
/api/rules:
|
||||
get:
|
||||
tags: [Rules]
|
||||
summary: List rules
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: EventCode
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: Filter by event code
|
||||
- name: Active
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
description: Filter by active flag
|
||||
- name: ScopeType
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [GLOBAL, TESTSITE]
|
||||
description: Filter by scope type
|
||||
- name: TestSiteID
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: Filter by TestSiteID (for TESTSITE scope)
|
||||
- name: search
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: Search by rule name
|
||||
responses:
|
||||
'200':
|
||||
description: List of rules
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../components/schemas/rules.yaml#/RuleDef'
|
||||
|
||||
post:
|
||||
tags: [Rules]
|
||||
summary: Create rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Name:
|
||||
type: string
|
||||
Description:
|
||||
type: string
|
||||
EventCode:
|
||||
type: string
|
||||
example: ORDER_CREATED
|
||||
ScopeType:
|
||||
type: string
|
||||
enum: [GLOBAL, TESTSITE]
|
||||
TestSiteID:
|
||||
type: integer
|
||||
nullable: true
|
||||
ConditionExpr:
|
||||
type: string
|
||||
nullable: true
|
||||
Priority:
|
||||
type: integer
|
||||
Active:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
Seq:
|
||||
type: integer
|
||||
ActionType:
|
||||
type: string
|
||||
ActionParams:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: object
|
||||
required: [Name, EventCode, ScopeType]
|
||||
responses:
|
||||
'201':
|
||||
description: Rule created
|
||||
|
||||
/api/rules/{id}:
|
||||
get:
|
||||
tags: [Rules]
|
||||
summary: Get rule (with actions)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Rule details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
$ref: '../components/schemas/rules.yaml#/RuleWithActions'
|
||||
'404':
|
||||
description: Rule not found
|
||||
|
||||
patch:
|
||||
tags: [Rules]
|
||||
summary: Update rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Name: { type: string }
|
||||
Description: { type: string }
|
||||
EventCode: { type: string }
|
||||
ScopeType: { type: string, enum: [GLOBAL, TESTSITE] }
|
||||
TestSiteID: { type: integer, nullable: true }
|
||||
ConditionExpr: { type: string, nullable: true }
|
||||
Priority: { type: integer }
|
||||
Active: { type: integer, enum: [0, 1] }
|
||||
responses:
|
||||
'200':
|
||||
description: Rule updated
|
||||
'404':
|
||||
description: Rule not found
|
||||
|
||||
delete:
|
||||
tags: [Rules]
|
||||
summary: Soft delete rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Rule deleted
|
||||
'404':
|
||||
description: Rule not found
|
||||
|
||||
/api/rules/validate:
|
||||
post:
|
||||
tags: [Rules]
|
||||
summary: Validate/evaluate an expression
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
expr:
|
||||
type: string
|
||||
context:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required: [expr]
|
||||
responses:
|
||||
'200':
|
||||
description: Validation result
|
||||
|
||||
/api/rules/{id}/actions:
|
||||
get:
|
||||
tags: [Rules]
|
||||
summary: List actions for a rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Actions list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../components/schemas/rules.yaml#/RuleAction'
|
||||
|
||||
post:
|
||||
tags: [Rules]
|
||||
summary: Create action for a rule
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Seq:
|
||||
type: integer
|
||||
ActionType:
|
||||
type: string
|
||||
ActionParams:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: object
|
||||
required: [ActionType]
|
||||
responses:
|
||||
'201':
|
||||
description: Action created
|
||||
|
||||
/api/rules/{id}/actions/{actionId}:
|
||||
patch:
|
||||
tags: [Rules]
|
||||
summary: Update action
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: actionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
Seq: { type: integer }
|
||||
ActionType: { type: string }
|
||||
ActionParams:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: object
|
||||
responses:
|
||||
'200':
|
||||
description: Action updated
|
||||
|
||||
delete:
|
||||
tags: [Rules]
|
||||
summary: Soft delete action
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: actionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Action deleted
|
||||
24
tests/unit/Rules/RuleExpressionServiceTest.php
Normal file
24
tests/unit/Rules/RuleExpressionServiceTest.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Rules;
|
||||
|
||||
use App\Services\RuleExpressionService;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class RuleExpressionServiceTest extends CIUnitTestCase
|
||||
{
|
||||
public function testEvaluateBooleanWithArrayContext(): void
|
||||
{
|
||||
$svc = new RuleExpressionService();
|
||||
|
||||
$ok = $svc->evaluateBoolean('order["SiteID"] == 1', [
|
||||
'order' => ['SiteID' => 1],
|
||||
]);
|
||||
$this->assertTrue($ok);
|
||||
|
||||
$no = $svc->evaluateBoolean('order["SiteID"] == 2', [
|
||||
'order' => ['SiteID' => 1],
|
||||
]);
|
||||
$this->assertFalse($no);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user