The CLQMS Rule Engine evaluates business rules that inspect orders, patients, and tests, then executes actions when the compiled condition matches.
Rules are authored using a domain specific language stored in `ruledef.ConditionExpr`. Before the platform executes any rule, the DSL must be compiled into JSON and stored in `ConditionExprCompiled`, and each rule must be linked to the tests it should influence via `testrule`.
3. Save the compiled payload into `ConditionExprCompiled` and persist the rule in `ruledef`.
4. Link the rule to one or more tests through `testrule.TestSiteID` (rules only run for linked tests).
5. When the configured event fires (`test_created` or `result_updated`), the engine evaluates `ConditionExprCompiled` and runs the resulting `then` or `else` actions.
> **Note:** The rule engine currently fires only for `test_created` and `result_updated`. Other event codes can exist in the database but are not triggered by the application unless additional `RuleEngineService::run(...)` calls are added.
## Event Triggers
| Event Code | Status | Trigger Point |
|------------|--------|----------------|
| `test_created` | Active | Fired after a new test row is persisted; the handler calls `RuleEngineService::run('test_created', ...)` to evaluate test-scoped rules |
| `result_updated` | Active | Fired whenever a test result is saved or updated so result-dependent rules run immediately |
Other event codes remain in the database for future workflows, but only `test_created` and `result_updated` are executed by the current application flow.
The DSL expression lives in `ConditionExpr`. The compile endpoint (`/api/rule/compile`) renders the lifeblood of execution, producing `conditionExpr`, `valueExpr`, `then`, and `else` nodes that the engine consumes at runtime.
- Use `&&` for AND (all sub-conditions must match).
- Use `||` for OR (any matching branch satisfies the rule).
- Surround mixed logic with parentheses for clarity and precedence.
### Multi-Action Syntax
Actions within any branch are separated by `:` and evaluated in order. Every `then` and `else` branch must end with an action; use `nothing` when no further work is required.
Create each rule as its own `ruledef` row; do not chain expressions with commas. The `testrule` table manages rule-to-test mappings, so multiple rules can attach to the same test. Example:
1. Insert `RULE_MALE_RESULT` and `RULE_SENIOR_COMMENT` in `ruledef`.
2. Add two `testrule` rows linking each rule to the appropriate `TestSiteID`.
Each rule compiles and runs independently when its trigger fires and the test is linked.
## Available Functions
### Conditions
| Function | Description | Example |
|----------|-------------|---------|
| `sex('M'|'F')` | Match patient sex | `sex('M')` |
| `priority('R'|'S'|'U')` | Match order priority | `priority('S')` |
| `requested('CODE')` | Check whether the order already requested a test (queries `patres`) | `requested('GLU')` |
### Logical Operators
| Operator | Meaning | Example |
|----------|---------|---------|
| `&&` | AND (all truthy) | `sex('M') && age > 40` |
| `||` | OR (any truthy) | `sex('M') || age > 65` |
| `()` | Group expressions | `(sex('M') && age > 40) || priority('S')` |
## Actions
| Action | Description | Example |
|--------|-------------|---------|
| `result_set(value)` | Write to `patres.Result` for the current order/test using the provided value | `result_set(0.5)` |
| `test_insert('CODE')` | Insert a test row by `TestSiteCode` if it doesn’t already exist for the order | `test_insert('HBA1C')` |
| `test_delete('CODE')` | Remove a previously requested test from the current order when the rule deems it unnecessary | `test_delete('INS')` |
| `comment_insert('text')` | Insert an order comment (`ordercom`) describing priority or clinical guidance | `comment_insert('Male patient - review')` |
| `nothing` | Explicit no-op to terminate an action chain | `nothing` |
> **Note:** `set_priority()` was removed. Use `comment_insert()` for priority notes without altering billing.
## Runtime Requirements
1.**Compiled expression required:** Rules without `ConditionExprCompiled` are ignored (see `RuleEngineService::run`).
2.**Order context:**`context.order.InternalOID` must exist for any action that writes to `patres` or `ordercom`.
3.**TestSiteID:**`result_set()` needs `testSiteID` (either provided in context or from `order.TestSiteID`). `test_insert()` resolves a `TestSiteID` via the `TestSiteCode` in `TestDefSiteModel`, and `test_delete()` removes the matching `TestSiteID` rows when needed.
4.**Requested check:**`requested('CODE')` inspects `patres` rows for the same `OrderID` and `TestSiteCode`.
-`RESULT_SET requires context.order.InternalOID` or `testSiteID`: include those fields in the context passed to `RuleEngineService::run()`.
-`TEST_INSERT` failures mean the provided `TestSiteCode` does not exist or the rule attempted to insert a duplicate test; check `testdefsite` and existing `patres` rows.
-`COMMENT_INSERT requires comment`: ensure the action provides text.