feat: update calc endpoints and rule docs
This commit is contained in:
parent
4bb5496073
commit
6ece30302f
BIN
.cocoindex_code/cocoindex.db/mdb/data.mdb
Normal file
BIN
.cocoindex_code/cocoindex.db/mdb/data.mdb
Normal file
Binary file not shown.
BIN
.cocoindex_code/cocoindex.db/mdb/lock.mdb
Normal file
BIN
.cocoindex_code/cocoindex.db/mdb/lock.mdb
Normal file
Binary file not shown.
41
.cocoindex_code/settings.yml
Normal file
41
.cocoindex_code/settings.yml
Normal file
@ -0,0 +1,41 @@
|
||||
exclude_patterns:
|
||||
- '**/.*'
|
||||
- '**/__pycache__'
|
||||
- '**/node_modules'
|
||||
- '**/target'
|
||||
- '**/build/assets'
|
||||
- '**/dist'
|
||||
- '**/vendor/*.*/*'
|
||||
- '**/vendor/*'
|
||||
- '**/.cocoindex_code'
|
||||
include_patterns:
|
||||
- '**/*.py'
|
||||
- '**/*.pyi'
|
||||
- '**/*.js'
|
||||
- '**/*.jsx'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.mjs'
|
||||
- '**/*.cjs'
|
||||
- '**/*.rs'
|
||||
- '**/*.go'
|
||||
- '**/*.java'
|
||||
- '**/*.c'
|
||||
- '**/*.h'
|
||||
- '**/*.cpp'
|
||||
- '**/*.hpp'
|
||||
- '**/*.cc'
|
||||
- '**/*.cxx'
|
||||
- '**/*.hxx'
|
||||
- '**/*.hh'
|
||||
- '**/*.cs'
|
||||
- '**/*.sql'
|
||||
- '**/*.sh'
|
||||
- '**/*.bash'
|
||||
- '**/*.zsh'
|
||||
- '**/*.md'
|
||||
- '**/*.mdx'
|
||||
- '**/*.txt'
|
||||
- '**/*.rst'
|
||||
- '**/*.php'
|
||||
- '**/*.lua'
|
||||
BIN
.cocoindex_code/target_sqlite.db
Normal file
BIN
.cocoindex_code/target_sqlite.db
Normal file
Binary file not shown.
1
.gitignore
vendored
1
.gitignore
vendored
@ -125,3 +125,4 @@ _modules/*
|
||||
/results/
|
||||
/phpunit*.xml
|
||||
/public/.htaccess
|
||||
|
||||
|
||||
2
.serena/.gitignore
vendored
2
.serena/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
/cache
|
||||
/project.local.yml
|
||||
@ -1,32 +0,0 @@
|
||||
# CLQMS Project Overview
|
||||
- **Name:** CLQMS (Clinical Laboratory Quality Management System)
|
||||
- **Type:** Headless REST API backend (no view layer for product UX)
|
||||
- **Purpose:** Manage clinical laboratory workflows (patients, orders, specimens, results, value sets, edge/instrument integration) via JSON APIs.
|
||||
- **Framework/Runtime:** CodeIgniter 4 on PHP 8.1+
|
||||
- **Database:** MySQL (legacy PascalCase column naming in many tables)
|
||||
- **Auth:** JWT (firebase/php-jwt), typically required for protected `/api/*` endpoints.
|
||||
|
||||
## Architecture Notes
|
||||
- API-first and frontend-agnostic; clients consume REST JSON endpoints.
|
||||
- Controllers delegate business logic to models/services; avoid direct DB query logic in controllers.
|
||||
- Standardized response format with `status`, `message`, `data`.
|
||||
- ValueSet/Lookups system supports static lookup data and API-managed lookup definitions.
|
||||
- OpenAPI docs live under `public/api-docs.yaml`, `public/paths/*.yaml`, `public/components/schemas/*.yaml` and are bundled into `public/api-docs.bundled.yaml`.
|
||||
|
||||
## High-Level Structure
|
||||
- `app/Config` - framework and app configuration (routes, filters, etc.)
|
||||
- `app/Controllers` - REST controllers
|
||||
- `app/Models` - data access and DB logic
|
||||
- `app/Services` - service-layer logic
|
||||
- `app/Filters` - auth/request filters
|
||||
- `app/Helpers` - helper functions (including UTC handling per conventions)
|
||||
- `app/Libraries` - shared libraries (lookups/valuesets, etc.)
|
||||
- `app/Traits` - reusable traits (including response behavior)
|
||||
- `tests/feature`, `tests/unit` - PHPUnit test suites
|
||||
- `public/paths`, `public/components/schemas` - modular OpenAPI source files
|
||||
|
||||
## Key Dependencies
|
||||
- `codeigniter4/framework`
|
||||
- `firebase/php-jwt`
|
||||
- `mossadal/math-parser`
|
||||
- Dev: `phpunit/phpunit`, `fakerphp/faker`
|
||||
@ -1,43 +0,0 @@
|
||||
# CLQMS Style and Conventions
|
||||
## PHP and Naming
|
||||
- PHP 8.1+, PSR-4 autoloading (`App\\` => `app/`, `Config\\` => `app/Config/`).
|
||||
- Follow PSR-12 where applicable.
|
||||
- Class names: PascalCase.
|
||||
- Method names: camelCase.
|
||||
- Properties: legacy snake_case and newer camelCase coexist.
|
||||
- Constants: UPPER_SNAKE_CASE.
|
||||
- DB tables: snake_case; many DB columns are legacy PascalCase.
|
||||
- JSON fields in API responses often use PascalCase for domain fields.
|
||||
|
||||
## Controller Pattern
|
||||
- Controllers should handle HTTP concerns and delegate to model/service logic.
|
||||
- Avoid embedding DB query logic directly inside controllers.
|
||||
- Use `ResponseTrait` and consistent JSON envelope responses.
|
||||
|
||||
## Response Pattern
|
||||
- Success: `status=success`, plus message/data.
|
||||
- Error: structured error response with proper HTTP status codes.
|
||||
- Empty strings may be normalized to `null` by custom response behavior.
|
||||
|
||||
## DB and Data Handling
|
||||
- Prefer CodeIgniter Model/Query Builder usage.
|
||||
- Use UTC helper conventions for datetime handling.
|
||||
- Multi-table writes should be wrapped in transactions.
|
||||
- For nested entities/arrays, extract and handle nested payloads carefully before filtering and persistence.
|
||||
|
||||
## Error Handling
|
||||
- Use try/catch for JWT and external operations.
|
||||
- Log errors with `log_message('error', ...)`.
|
||||
- Return structured API errors with correct status codes.
|
||||
|
||||
## Testing Convention
|
||||
- PHPUnit tests in `tests/`.
|
||||
- Test naming: `test<Action><Scenario><ExpectedResult>`.
|
||||
- Typical status expectation: 200 (GET/PATCH), 201 (POST), 400/401/404/500 as appropriate.
|
||||
|
||||
## API Docs Rule (Critical)
|
||||
- Any controller/API contract change must update corresponding OpenAPI YAML files under:
|
||||
- `public/paths/*.yaml`
|
||||
- `public/components/schemas/*.yaml`
|
||||
- optionally `public/api-docs.yaml` if references/tags change
|
||||
- Rebuild docs bundle after YAML updates.
|
||||
@ -1,32 +0,0 @@
|
||||
# Suggested Commands (Windows)
|
||||
## Core Project Commands
|
||||
- Install dependencies: `composer install`
|
||||
- Run all tests: `./vendor/bin/phpunit`
|
||||
- Run one test file: `./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php`
|
||||
- Run one test method: `./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php`
|
||||
- Run test suite: `./vendor/bin/phpunit --testsuite App`
|
||||
- Run with coverage: `./vendor/bin/phpunit --coverage-html build/logs/html`
|
||||
|
||||
## CodeIgniter/Spark Commands
|
||||
- Create migration: `php spark make:migration <name>`
|
||||
- Create model: `php spark make:model <name>`
|
||||
- Create controller: `php spark make:controller <name>`
|
||||
- Apply migrations: `php spark migrate`
|
||||
- Rollback migrations: `php spark migrate:rollback`
|
||||
- Run local app: `php spark serve`
|
||||
|
||||
## OpenAPI Docs Commands
|
||||
- Rebundle API docs after YAML changes: `node public/bundle-api-docs.js`
|
||||
|
||||
## Git and Shell Utilities (Windows)
|
||||
- Git status: `git status`
|
||||
- Diff changes: `git diff`
|
||||
- Show staged diff: `git diff --cached`
|
||||
- Recent commits: `git log --oneline -n 10`
|
||||
- List files (PowerShell): `Get-ChildItem`
|
||||
- Recursive file search (PowerShell): `Get-ChildItem -Recurse -File`
|
||||
- Text search (PowerShell): `Select-String -Path .\* -Pattern "<text>" -Recurse`
|
||||
|
||||
## Notes
|
||||
- Testing DB values in `phpunit.xml.dist` are environment-specific; verify before running tests.
|
||||
- API docs bundle output file: `public/api-docs.bundled.yaml`.
|
||||
@ -1,9 +0,0 @@
|
||||
# Task Completion Checklist
|
||||
When finishing a coding change in CLQMS:
|
||||
|
||||
1. Run targeted tests first (file/method-level), then broader PHPUnit suite if scope warrants it.
|
||||
2. Verify API response structure consistency (`status`, `message`, `data`) and proper HTTP status codes.
|
||||
3. If controllers or API contracts changed, update OpenAPI YAML files in `public/paths` and/or `public/components/schemas`.
|
||||
4. Rebundle OpenAPI docs with `node public/bundle-api-docs.js` after YAML updates.
|
||||
5. Confirm no secrets/credentials were introduced in tracked files.
|
||||
6. Review diff for legacy field naming compatibility (PascalCase DB columns/JSON domain fields where expected).
|
||||
@ -1,135 +0,0 @@
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "clqms01-be"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- php
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
@ -152,7 +152,8 @@ $routes->group('api', function ($routes) {
|
||||
});
|
||||
});
|
||||
|
||||
$routes->post('calc/(:any)', 'CalculatorController::calculateByCodeOrName/$1');
|
||||
$routes->post('calc/testsite/(:num)', 'CalculatorController::calculateByTestSite/$1');
|
||||
$routes->post('calc/testcode/(:any)', 'CalculatorController::calculateByCodeOrName/$1');
|
||||
|
||||
// Counter
|
||||
$routes->group('counter', function ($routes) {
|
||||
@ -307,9 +308,9 @@ $routes->group('api', function ($routes) {
|
||||
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
|
||||
});
|
||||
|
||||
// Test Mapping
|
||||
// Test
|
||||
$routes->group('test', function ($routes) {
|
||||
$routes->get('/', 'Test\TestsController::index');
|
||||
$routes->get('/', 'Test\TestsController::index');
|
||||
$routes->get('(:num)', 'Test\TestsController::show/$1');
|
||||
$routes->post('/', 'Test\TestsController::create');
|
||||
$routes->patch('(:num)', 'Test\TestsController::update/$1');
|
||||
@ -349,7 +350,7 @@ $routes->group('api', function ($routes) {
|
||||
});
|
||||
|
||||
// Rules
|
||||
$routes->group('rule', function ($routes) {
|
||||
$routes->group('rule', ['filter' => 'auth'], function ($routes) {
|
||||
$routes->get('/', 'Rule\RuleController::index');
|
||||
$routes->get('(:num)', 'Rule\RuleController::show/$1');
|
||||
$routes->post('/', 'Rule\RuleController::create');
|
||||
@ -359,6 +360,7 @@ $routes->group('api', function ($routes) {
|
||||
$routes->post('compile', 'Rule\RuleController::compile');
|
||||
});
|
||||
|
||||
|
||||
// Demo/Test Routes (No Auth)
|
||||
$routes->group('api/demo', function ($routes) {
|
||||
$routes->post('order', 'Test\DemoOrderController::createDemoOrder');
|
||||
|
||||
@ -108,7 +108,7 @@ class CalculatorController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* POST api/calculate/test-site/{testSiteID}
|
||||
* POST api/calc/testsite/{testSiteID}
|
||||
* Calculate using TestDefCal definition
|
||||
*
|
||||
* Request: {
|
||||
@ -151,7 +151,7 @@ class CalculatorController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* POST api/calc/{codeOrName}
|
||||
* POST api/calc/testcode/{codeOrName}
|
||||
* Evaluate a configured calculation by its code or name and return only the result map.
|
||||
*/
|
||||
public function calculateByCodeOrName($codeOrName): ResponseInterface
|
||||
|
||||
@ -9,7 +9,7 @@ class CreateTestDefinitions extends Migration {
|
||||
$this->forge->addField([
|
||||
'TestSiteID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteCode' => ['type' => 'varchar', 'constraint'=> 6, 'null' => false],
|
||||
'TestSiteCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => false],
|
||||
'TestSiteName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => false],
|
||||
'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
|
||||
'Description' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||
|
||||
@ -85,32 +85,9 @@ class CreateSpecimens extends Migration {
|
||||
$this->forge->addKey('SpcPrpID', true);
|
||||
$this->forge->createTable('specimenprep');
|
||||
|
||||
$this->forge->addField([
|
||||
'SpcLogID'=> ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
|
||||
'TblName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'RecID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'FldName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'FldValuePrev'=> ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'UserID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'SiteID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'DIDType' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'DID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'MachineID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'SessionID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'AppID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'ProcessID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'WebPageID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'EventID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'ActivityID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'Reason' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'LogDate' => ['type' => 'Datetime', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('SpcLogID', true);
|
||||
$this->forge->createTable('specimenlog');
|
||||
}
|
||||
|
||||
public function down() {
|
||||
$this->forge->dropTable('specimenlog');
|
||||
$this->forge->dropTable('specimenprep');
|
||||
$this->forge->dropTable('specimencollection');
|
||||
$this->forge->dropTable('specimenstatus');
|
||||
|
||||
@ -95,32 +95,9 @@ class CreatePatientCore extends Migration {
|
||||
$this->forge->addKey('PatRelID', true);
|
||||
$this->forge->createTable('patrelation');
|
||||
|
||||
$this->forge->addField([
|
||||
'PatRegLogID'=> ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
|
||||
'TblName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'RecID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'FldName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'FldValuePrev'=> ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'UserID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'SiteID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'DIDType' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'DID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'MachineID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'SessionID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'AppID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'ProcessID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'WebPageID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'EventID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'ActivityID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'Reason' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'LogDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('PatRegLogID', true);
|
||||
$this->forge->createTable('patreglog');
|
||||
}
|
||||
|
||||
public function down() {
|
||||
$this->forge->dropTable('patreglog');
|
||||
$this->forge->dropTable('patrelation');
|
||||
$this->forge->dropTable('patidt');
|
||||
$this->forge->dropTable('patcom');
|
||||
|
||||
@ -52,33 +52,9 @@ class CreatePatientVisits extends Migration {
|
||||
$this->forge->addKey('PVADTID', true);
|
||||
$this->forge->createTable('patvisitadt');
|
||||
|
||||
$this->forge->addField([
|
||||
'PatVisLogID'=> ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
|
||||
'TblName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'RecID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'FldName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'FldValuePrev'=> ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'Origin' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'UserID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'SiteID' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'DIDType' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'DID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'MachineID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'SessionID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'AppID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'ProcessID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'WebPageID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'EventID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'ActivityID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'Reason' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'LogDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('PatVisLogID', true);
|
||||
$this->forge->createTable('patvisitlog');
|
||||
}
|
||||
|
||||
public function down() {
|
||||
$this->forge->dropTable('patvisitlog');
|
||||
$this->forge->dropTable('patvisitadt');
|
||||
$this->forge->dropTable('patdiag');
|
||||
$this->forge->dropTable('patvisit');
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Database\Seeds;
|
||||
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Services\RuleExpressionService;
|
||||
|
||||
class TestSeeder extends Seeder
|
||||
{
|
||||
@ -192,19 +193,19 @@ class TestSeeder extends Seeder
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'EGFR', 'TestSiteName' => 'eGFR (CKD-EPI)', 'TestType' => 'CALC', 'Description' => 'Estimated Glomerular Filtration Rate', 'SeqScr' => '190', 'SeqRpt' => '190', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['EGFR'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['EGFR'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaCode' => 'CKD_EPI(CREA,AGE,GENDER)', 'RefType' => 'RANGE', 'Unit1' => 'mL/min/1.73m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['EGFR'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaCode' => '142 * (({CREA}/{KAPPA}) < 1 ? ({CREA}/{KAPPA}) : 1) ** {ALPHA} * (({CREA}/{KAPPA}) > 1 ? ({CREA}/{KAPPA}) : 1) ** -1.200 * (0.9938 ** {AGE}) * {SEXFAC}', 'RefType' => 'RANGE', 'Unit1' => 'mL/min/1.73m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefcal')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'LDLCALC', 'TestSiteName' => 'LDL Cholesterol (Calculated)', 'TestType' => 'CALC', 'Description' => 'Friedewald formula: TC - HDL - (TG/5)', 'SeqScr' => '200', 'SeqRpt' => '200', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['LDLCALC'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['LDLCALC'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaCode' => 'CHOL - HDL - (TG/5)', 'RefType' => 'RANGE', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['LDLCALC'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaCode' => '{CHOL} - {HDL} - ({TG}/5)', 'RefType' => 'RANGE', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefcal')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'IBIL', 'TestSiteName' => 'Indirect Bilirubin', 'TestType' => 'CALC', 'Description' => 'Bilirubin Indirek: TBIL - DBIL', 'SeqScr' => '210', 'SeqRpt' => '210', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['IBIL'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['IBIL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaCode' => 'TBIL - DBIL', 'RefType' => 'RANGE', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['IBIL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaCode' => '{TBIL} - {DBIL}', 'RefType' => 'RANGE', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefcal')->insert($data);
|
||||
|
||||
// CALC dependencies are grouped via testdefgrp
|
||||
@ -288,6 +289,18 @@ class TestSeeder extends Seeder
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['AGE'] = $this->db->insertID();
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'ALPHA', 'TestSiteName' => 'Alpha', 'TestType' => 'PARAM', 'Description' => 'eGFR Alpha', 'SeqScr' => '35', 'SeqRpt' => '35', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '3', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['ALPHA'] = $this->db->insertID();
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'KAPPA', 'TestSiteName' => 'Kappa', 'TestType' => 'PARAM', 'Description' => 'eGFR Kappa', 'SeqScr' => '36', 'SeqRpt' => '36', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['KAPPA'] = $this->db->insertID();
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'SEXFAC', 'TestSiteName' => 'Sex Factor', 'TestType' => 'PARAM', 'Description' => 'eGFR Sex Factor', 'SeqScr' => '37', 'SeqRpt' => '37', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '3', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SEXFAC'] = $this->db->insertID();
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'SYSTL', 'TestSiteName' => 'Systolic BP', 'TestType' => 'PARAM', 'Description' => 'Tekanan Darah Sistolik', 'SeqScr' => '40', 'SeqRpt' => '40', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SYSTL'] = $this->db->insertID();
|
||||
@ -302,15 +315,72 @@ class TestSeeder extends Seeder
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'BMI', 'TestSiteName' => 'Body Mass Index', 'TestType' => 'CALC', 'Description' => 'Indeks Massa Tubuh - weight/(height^2)', 'SeqScr' => '100', 'SeqRpt' => '100', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['BMI'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['BMI'], 'DisciplineID' => '10', 'DepartmentID' => '', 'FormulaCode' => 'WEIGHT / ((HEIGHT/100) * (HEIGHT/100))', 'RefType' => 'RANGE', 'Unit1' => 'kg/m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['BMI'], 'DisciplineID' => '10', 'DepartmentID' => '', 'FormulaCode' => '{WEIGHT} / (({HEIGHT}/100) * ({HEIGHT}/100))', 'RefType' => 'RANGE', 'Unit1' => 'kg/m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefcal')->insert($data);
|
||||
|
||||
$this->db->table('testdefgrp')->insertBatch([
|
||||
['TestSiteID' => $tIDs['BMI'], 'Member' => $tIDs['WEIGHT'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['BMI'], 'Member' => $tIDs['HEIGHT'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['EGFR'], 'Member' => $tIDs['AGE'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['EGFR'], 'Member' => $tIDs['ALPHA'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['EGFR'], 'Member' => $tIDs['KAPPA'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['EGFR'], 'Member' => $tIDs['SEXFAC'], 'CreateDate' => "$now"],
|
||||
]);
|
||||
|
||||
// ========================================
|
||||
// RULES: Auto-fill ALPHA/KAPPA by sex
|
||||
// ========================================
|
||||
$ruleExpr = new RuleExpressionService();
|
||||
$ruleDefs = [
|
||||
[
|
||||
'RuleCode' => 'ALPHA_SEX',
|
||||
'RuleName' => 'Set ALPHA from sex',
|
||||
'Description' => 'Set ALPHA based on patient sex (M=-0.302, F/other=-0.241)',
|
||||
'EventCode' => 'test_created',
|
||||
'TestSiteID' => $tIDs['ALPHA'],
|
||||
'Expr' => "if(sex('M'); set_result(-0.302); set_result(-0.241))",
|
||||
],
|
||||
[
|
||||
'RuleCode' => 'KAPPA_SEX',
|
||||
'RuleName' => 'Set KAPPA from sex',
|
||||
'Description' => 'Set KAPPA based on patient sex (M=0.9, F/other=0.7)',
|
||||
'EventCode' => 'test_created',
|
||||
'TestSiteID' => $tIDs['KAPPA'],
|
||||
'Expr' => "if(sex('M'); set_result(0.9); set_result(0.7))",
|
||||
],
|
||||
[
|
||||
'RuleCode' => 'SEXFAC_SEX',
|
||||
'RuleName' => 'Set SEXFAC from sex',
|
||||
'Description' => 'Set SEXFAC based on patient sex (F=1.012, M/other=1)',
|
||||
'EventCode' => 'test_created',
|
||||
'TestSiteID' => $tIDs['SEXFAC'],
|
||||
'Expr' => "if(sex('F'); set_result(1.012); set_result(1))",
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($ruleDefs as $ruleDef) {
|
||||
$compiled = $ruleExpr->compile($ruleDef['Expr']);
|
||||
$data = [
|
||||
'RuleCode' => $ruleDef['RuleCode'],
|
||||
'RuleName' => $ruleDef['RuleName'],
|
||||
'Description' => $ruleDef['Description'],
|
||||
'EventCode' => $ruleDef['EventCode'],
|
||||
'ConditionExpr' => $ruleDef['Expr'],
|
||||
'ConditionExprCompiled' => json_encode($compiled),
|
||||
'CreateDate' => $now,
|
||||
'StartDate' => null,
|
||||
'EndDate' => null,
|
||||
];
|
||||
$this->db->table('ruledef')->insert($data);
|
||||
$ruleID = $this->db->insertID();
|
||||
|
||||
$this->db->table('testrule')->insert([
|
||||
'RuleID' => $ruleID,
|
||||
'TestSiteID' => $ruleDef['TestSiteID'],
|
||||
'CreateDate' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TEST MAP - Specimen Mapping
|
||||
// ========================================
|
||||
|
||||
@ -2,13 +2,10 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use MathParser\StdMathParser;
|
||||
use MathParser\Interpreting\Evaluator;
|
||||
use MathParser\Exceptions\MathParserException;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
|
||||
class CalculatorService {
|
||||
protected StdMathParser $parser;
|
||||
protected Evaluator $evaluator;
|
||||
protected ExpressionLanguage $language;
|
||||
|
||||
/**
|
||||
* Gender mapping for calculations
|
||||
@ -24,8 +21,7 @@ class CalculatorService {
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
$this->parser = new StdMathParser();
|
||||
$this->evaluator = new Evaluator();
|
||||
$this->language = new ExpressionLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,21 +35,14 @@ class CalculatorService {
|
||||
public function calculate(string $formula, array $variables = []): ?float {
|
||||
try {
|
||||
$normalizedFormula = $this->normalizeFormulaVariables($formula, $variables);
|
||||
$expression = $this->prepareExpression($normalizedFormula, $variables);
|
||||
[$expression, $context] = $this->prepareExpression($normalizedFormula, $variables);
|
||||
|
||||
// Parse the expression
|
||||
$ast = $this->parser->parse($expression);
|
||||
|
||||
// Evaluate
|
||||
$result = $ast->accept($this->evaluator);
|
||||
$result = $this->language->evaluate($expression, $context);
|
||||
|
||||
return (float) $result;
|
||||
} catch (MathParserException $e) {
|
||||
log_message('error', 'MathParser error: ' . $e->getMessage() . ' | Formula: ' . $formula);
|
||||
throw new \Exception('Invalid formula: ' . $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Calculator error: ' . $e->getMessage() . ' | Formula: ' . $formula);
|
||||
throw $e;
|
||||
throw new \Exception('Invalid formula: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,12 +53,15 @@ class CalculatorService {
|
||||
* @return array ['valid' => bool, 'error' => string|null]
|
||||
*/
|
||||
public function validate(string $formula): array {
|
||||
$formula = $this->normalizeFormulaVariables($formula, []);
|
||||
$variables = array_fill_keys($this->extractVariables($formula), 1);
|
||||
|
||||
try {
|
||||
// Replace placeholders with dummy values for validation
|
||||
$testExpression = preg_replace('/\{([^}]+)\}/', '1', $formula);
|
||||
$this->parser->parse($testExpression);
|
||||
[$expression, $context] = $this->prepareExpression($formula, $variables);
|
||||
$this->language->evaluate($expression, $context);
|
||||
|
||||
return ['valid' => true, 'error' => null];
|
||||
} catch (MathParserException $e) {
|
||||
} catch (\Throwable $e) {
|
||||
return ['valid' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
@ -88,31 +80,49 @@ class CalculatorService {
|
||||
/**
|
||||
* Prepare expression by replacing placeholders with values
|
||||
*/
|
||||
protected function prepareExpression(string $formula, array $variables): string {
|
||||
protected function prepareExpression(string $formula, array $variables): array {
|
||||
$expression = $formula;
|
||||
$context = [];
|
||||
$usedNames = [];
|
||||
|
||||
foreach ($variables as $key => $value) {
|
||||
$placeholder = '{' . $key . '}';
|
||||
|
||||
// Handle gender specially
|
||||
if ($key === 'gender') {
|
||||
$value = $this->normalizeGender($value);
|
||||
}
|
||||
|
||||
// Ensure numeric value
|
||||
if (!is_numeric($value)) {
|
||||
throw new \Exception("Variable '{$key}' must be numeric, got: " . var_export($value, true));
|
||||
}
|
||||
|
||||
$expression = str_replace($placeholder, (float) $value, $expression);
|
||||
$variableName = $this->getSafeVariableName($key, $usedNames);
|
||||
$usedNames[] = $variableName;
|
||||
$context[$variableName] = (float) $value;
|
||||
|
||||
$expression = str_replace($placeholder, $variableName, $expression);
|
||||
}
|
||||
|
||||
// Check for unreplaced placeholders
|
||||
if (preg_match('/\{([^}]+)\}/', $expression, $unreplaced)) {
|
||||
throw new \Exception("Missing variable value for: {$unreplaced[1]}");
|
||||
}
|
||||
|
||||
return $expression;
|
||||
return [$expression, $context];
|
||||
}
|
||||
|
||||
protected function getSafeVariableName(string $key, array $usedNames): string {
|
||||
$base = preg_replace('/[^A-Za-z0-9_]/', '_', (string) $key);
|
||||
if ($base === '' || ctype_digit($base[0])) {
|
||||
$base = 'v_' . $base;
|
||||
}
|
||||
|
||||
$name = $base;
|
||||
$suffix = 1;
|
||||
while (in_array($name, $usedNames, true)) {
|
||||
$name = $base . '_' . $suffix++;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
"php": "^8.1",
|
||||
"codeigniter4/framework": "^4.0",
|
||||
"firebase/php-jwt": "^6.11",
|
||||
"mossadal/math-parser": "^1.3",
|
||||
"symfony/expression-language": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
53
composer.lock
generated
53
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": "b111968eaeab80698adb5ca0eaeeb8c1",
|
||||
"content-hash": "ad96435c829500be9b0bb553558894ea",
|
||||
"packages": [
|
||||
{
|
||||
"name": "codeigniter4/framework",
|
||||
@ -204,57 +204,6 @@
|
||||
],
|
||||
"time": "2025-05-06T19:29:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mossadal/math-parser",
|
||||
"version": "v1.3.16",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mossadal/math-parser.git",
|
||||
"reference": "981b03ca603fd281049e092d75245ac029e13dec"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mossadal/math-parser/zipball/981b03ca603fd281049e092d75245ac029e13dec",
|
||||
"reference": "981b03ca603fd281049e092d75245ac029e13dec",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpdocumentor/phpdocumentor": "2.*",
|
||||
"phpunit/php-code-coverage": "6.0.*",
|
||||
"phpunit/phpunit": "7.3.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MathParser\\": "src/MathParser"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank Wikström",
|
||||
"email": "frank@mossadal.se",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP parser for mathematical expressions, including elementary functions, variables and implicit multiplication. Also supports symbolic differentiation.",
|
||||
"homepage": "https://github.com/mossadal/math-parser",
|
||||
"keywords": [
|
||||
"mathematics",
|
||||
"parser"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/mossadal/math-parser/issues",
|
||||
"source": "https://github.com/mossadal/math-parser/tree/master"
|
||||
},
|
||||
"time": "2018-09-15T22:20:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "3.0.0",
|
||||
|
||||
194
docs/audit-logging.md
Normal file
194
docs/audit-logging.md
Normal file
@ -0,0 +1,194 @@
|
||||
# Audit Logging Strategy
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines how CLQMS should capture audit and operational logs across four tables:
|
||||
|
||||
- `logpatient` — patient, visit, and ADT activity
|
||||
- `logorder` — orders, tests, specimens, results, and QC
|
||||
- `logmaster` — master data and configuration changes
|
||||
- `logsystem` — sessions, security, import/export, and system operations
|
||||
|
||||
The intent is to audit all domains, including master data changes, and to standardize event capture so reporting and compliance are consistent.
|
||||
|
||||
## Table Ownership
|
||||
|
||||
| Event | Table |
|
||||
| --- | --- |
|
||||
| Patient registered/updated/merged | `logpatient` |
|
||||
| Insurance/consent changed | `logpatient` |
|
||||
| Patient visit (admit/transfer/discharge) | `logpatient` |
|
||||
| Order created/cancelled | `logorder` |
|
||||
| Sample received/rejected | `logorder` |
|
||||
| Result entered/verified/amended | `logorder` |
|
||||
| Result released/retracted/corrected | `logorder` |
|
||||
| QC result recorded | `logorder` |
|
||||
| Test panel added/removed | `logmaster` |
|
||||
| Reference range changed | `logmaster` |
|
||||
| Analyzer config updated | `logmaster` |
|
||||
| User role changed | `logmaster` |
|
||||
| User login/logout | `logsystem` |
|
||||
| Import/export job start/end | `logsystem` |
|
||||
|
||||
## Standard Log Schema (Shared Columns)
|
||||
|
||||
Use a shared schema for all four tables to keep instrumentation and reporting consistent. The legacy names below match existing patterns and can be reused.
|
||||
|
||||
| Column | Description |
|
||||
| --- | --- |
|
||||
| `LogID` (PK) | Auto increment primary key per table (e.g., `LogPatientID`) |
|
||||
| `TblName` | Source table name |
|
||||
| `RecID` | Record ID of the entity |
|
||||
| `FldName` | Field name that changed (nullable for bulk events) |
|
||||
| `FldValuePrev` | Previous value (string or JSON) |
|
||||
| `FldValueNew` | New value (string or JSON) |
|
||||
| `UserID` | Acting user ID (nullable for system actions) |
|
||||
| `SiteID` | Site context |
|
||||
| `DIDType` | Device identifier type |
|
||||
| `DID` | Device identifier |
|
||||
| `MachineID` | Workstation or host identifier |
|
||||
| `SessionID` | Session identifier |
|
||||
| `AppID` | Client application ID |
|
||||
| `ProcessID` | Process/workflow identifier |
|
||||
| `WebPageID` | UI page/context (nullable) |
|
||||
| `EventID` | Event code (see catalog) |
|
||||
| `ActivityID` | Action code (create/update/delete/read/etc.) |
|
||||
| `Reason` | User/system reason |
|
||||
| `LogDate` | Timestamp of event |
|
||||
| `Context` | JSON metadata (optional but recommended) |
|
||||
| `IpAddress` | Remote IP (optional but recommended) |
|
||||
|
||||
Recommended: keep a JSON string in `Context` for extra details (e.g., route, request id, batch id, error message). Use size limits to avoid oversized rows.
|
||||
|
||||
## Event Catalog
|
||||
|
||||
### logpatient
|
||||
|
||||
**Patient core**
|
||||
|
||||
- Register patient
|
||||
- Update demographics
|
||||
- Merge/unmerge/split
|
||||
- Identity changes (MRN, external identifiers)
|
||||
- Consent grant/revoke/update
|
||||
- Insurance add/update/remove
|
||||
- Patient record view (if required by compliance)
|
||||
|
||||
**Visit/ADT**
|
||||
|
||||
- Admit, transfer, discharge
|
||||
- Bed/ward/unit changes
|
||||
- Visit status updates
|
||||
|
||||
**Other**
|
||||
|
||||
- Patient notes/attachments added/removed
|
||||
- Patient alerts/flags changes
|
||||
|
||||
### logorder
|
||||
|
||||
**Orders/tests**
|
||||
|
||||
- Create/cancel/reopen order
|
||||
- Add/remove tests
|
||||
- Priority changes
|
||||
- Order comments added/removed
|
||||
|
||||
**Specimen lifecycle**
|
||||
|
||||
- Collected, labeled, received, rejected
|
||||
- Centrifuged, aliquoted, stored
|
||||
- Disposed/expired
|
||||
|
||||
**Results**
|
||||
|
||||
- Result entered/updated
|
||||
- Verified/amended
|
||||
- Released/retracted/corrected
|
||||
- Result comments/interpretation changes
|
||||
- Auto-verification override
|
||||
|
||||
**QC**
|
||||
|
||||
- QC result recorded
|
||||
- QC failure/override
|
||||
|
||||
### logmaster
|
||||
|
||||
**Value sets**
|
||||
|
||||
- Create/update/retire value set items
|
||||
|
||||
**Test definitions**
|
||||
|
||||
- Test definition updates (units, methods, ranges)
|
||||
- Reference range changes
|
||||
- Formula/delta check changes
|
||||
- Test panel membership add/remove
|
||||
|
||||
**Infrastructure**
|
||||
|
||||
- Analyzer/instrument config changes
|
||||
- Host app integration config
|
||||
- Coding system changes
|
||||
|
||||
**Users/roles**
|
||||
|
||||
- User create/disable/reset
|
||||
- Role changes
|
||||
- Permission changes
|
||||
|
||||
**Sites/workstations**
|
||||
|
||||
- Site/location/workstation CRUD
|
||||
|
||||
### logsystem
|
||||
|
||||
**Sessions & security**
|
||||
|
||||
- Login/logout
|
||||
- Failed login attempts
|
||||
- Lockouts/password resets
|
||||
- Token issue/refresh/revoke
|
||||
- Authorization failures
|
||||
|
||||
**Import/export**
|
||||
|
||||
- Import/export job start/end
|
||||
- Batch ID, source, record counts, status
|
||||
|
||||
**System operations**
|
||||
|
||||
- Background jobs start/end
|
||||
- Integration sync runs
|
||||
- System config changes
|
||||
- Service errors that affect data integrity
|
||||
|
||||
## Activity & Event Codes
|
||||
|
||||
Use consistent `ActivityID` and `EventID` values. Recommended defaults:
|
||||
|
||||
- `ActivityID`: `CREATE`, `UPDATE`, `DELETE`, `READ`, `MERGE`, `SPLIT`, `CANCEL`, `REOPEN`, `VERIFY`, `AMEND`, `RETRACT`, `RELEASE`, `IMPORT`, `EXPORT`, `LOGIN`, `LOGOUT`
|
||||
- `EventID`: domain-specific codes (e.g., `PATIENT_REGISTERED`, `ORDER_CREATED`, `RESULT_VERIFIED`, `QC_RECORDED`)
|
||||
|
||||
## Capture Guidelines
|
||||
|
||||
- Always capture `UserID`, `SessionID`, `SiteID`, and `LogDate` when available.
|
||||
- If the action is system-driven, set `UserID` to `SYSTEM` (or null) and add context in `Context`.
|
||||
- Store payload diffs in `FldValuePrev` and `FldValueNew` for single-field changes; for multi-field changes, put a JSON diff in `Context` and leave `FldName` null.
|
||||
- For bulk operations, store batch metadata in `Context` (`batch_id`, `record_count`, `source`).
|
||||
- Do not log secrets, tokens, or full PHI when not required. Mask or omit sensitive fields.
|
||||
|
||||
## Retention & Governance
|
||||
|
||||
- Define retention policy per table (e.g., 7 years for patient/order, 2 years for system).
|
||||
- Archive before purge; record purge activity in `logsystem`.
|
||||
- Restrict write/delete permissions to service accounts only.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
1. Create the four tables with shared schema (or migrate existing log tables to match).
|
||||
2. Add a single audit service with helpers to build a normalized payload.
|
||||
3. Instrument controllers/services for each event category above.
|
||||
4. Add automated tests for representative audit writes.
|
||||
5. Document `EventID` codes used by each endpoint/service.
|
||||
@ -2,7 +2,68 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The `CalculatorService` (`app/Services/CalculatorService.php`) uses the [mossadal/math-parser](https://github.com/mossadal/math-parser) library to safely evaluate mathematical expressions. This document lists all available operators, functions, and constants.
|
||||
The `CalculatorService` (`app/Services/CalculatorService.php`) evaluates formulas with Symfony's `ExpressionLanguage`. This document lists the operators, functions, and constants that are available in the current implementation.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints live under `/api` and accept JSON. Responses use the standard `{ status, message, data }` envelope unless stated otherwise.
|
||||
|
||||
### Calculate By Test Site
|
||||
|
||||
Uses the `testdefcal` definition for a test site. The incoming body supplies the variables required by the formula.
|
||||
|
||||
```http
|
||||
POST /api/calc/testsite/123
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"result": 85,
|
||||
"gender": "female",
|
||||
"age": 30
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"result": 92.4,
|
||||
"testSiteID": 123,
|
||||
"formula": "{result} * {factor} + {age}",
|
||||
"variables": {
|
||||
"result": 85,
|
||||
"gender": "female",
|
||||
"age": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Calculate By Code Or Name
|
||||
|
||||
Evaluates a configured calculation by `TestSiteCode` or `TestSiteName`. Returns a compact map with a single key/value or `{}` on failure.
|
||||
|
||||
```http
|
||||
POST /api/calc/testcode/GLU
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"result": 110,
|
||||
"factor": 1.1
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"GLU": 121
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -16,9 +77,34 @@ The `CalculatorService` (`app/Services/CalculatorService.php`) uses the [mossada
|
||||
| `-` | Subtraction | `10 - 4` | `6` |
|
||||
| `*` | Multiplication | `6 * 7` | `42` |
|
||||
| `/` | Division | `20 / 4` | `5` |
|
||||
| `^` | Exponentiation (power) | `2 ^ 3` | `8` |
|
||||
| `!` | Factorial | `5!` | `120` |
|
||||
| `!!` | Semi-factorial (double factorial) | `5!!` | `15` |
|
||||
| `%` | Modulo | `20 % 6` | `2` |
|
||||
| `**` | Exponentiation (power) | `2 ** 3` | `8` |
|
||||
|
||||
### Comparison Operators
|
||||
|
||||
| Operator | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `==` | Equal | `{result} == 10` |
|
||||
| `!=` | Not equal | `{result} != 10` |
|
||||
| `<` | Less than | `{result} < 10` |
|
||||
| `<=` | Less than or equal | `{result} <= 10` |
|
||||
| `>` | Greater than | `{result} > 10` |
|
||||
| `>=` | Greater than or equal | `{result} >= 10` |
|
||||
|
||||
### Logical Operators
|
||||
|
||||
| Operator | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `and` / `&&` | Logical AND | `{result} > 0 and {factor} > 0` |
|
||||
| `or` / `||` | Logical OR | `{gender} == 1 or {gender} == 2` |
|
||||
| `!` / `not` | Logical NOT | `not ({result} > 0)` |
|
||||
|
||||
### Conditional Operators
|
||||
|
||||
| Operator | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `?:` | Ternary | `{result} > 10 ? {result} : 10` |
|
||||
| `??` | Null coalescing | `{result} ?? 0` |
|
||||
|
||||
### Parentheses
|
||||
|
||||
@ -29,84 +115,35 @@ Use parentheses to control operation precedence:
|
||||
2 + 3 * 4 // Result: 14
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `^` is bitwise XOR (not exponentiation). Use `**` for powers.
|
||||
- Variables must be numeric after normalization (gender is mapped to 0/1/2).
|
||||
|
||||
---
|
||||
|
||||
## Mathematical Functions
|
||||
## Functions
|
||||
|
||||
### Rounding Functions
|
||||
Only the default ExpressionLanguage functions are available:
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sqrt(x)` | Square root | `sqrt(16)` → `4` |
|
||||
| `round(x)` | Round to nearest integer | `round(3.7)` → `4` |
|
||||
| `ceil(x)` | Round up to integer | `ceil(3.2)` → `4` |
|
||||
| `floor(x)` | Round down to integer | `floor(3.9)` → `3` |
|
||||
| `abs(x)` | Absolute value | `abs(-5)` → `5` |
|
||||
| `sgn(x)` | Sign function | `sgn(-10)` → `-1` |
|
||||
|
||||
### Trigonometric Functions (Radians)
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sin(x)` | Sine | `sin(pi/2)` → `1` |
|
||||
| `cos(x)` | Cosine | `cos(0)` → `1` |
|
||||
| `tan(x)` | Tangent | `tan(pi/4)` → `1` |
|
||||
| `cot(x)` | Cotangent | `cot(pi/4)` → `1` |
|
||||
|
||||
### Trigonometric Functions (Degrees)
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sind(x)` | Sine (degrees) | `sind(90)` → `1` |
|
||||
| `cosd(x)` | Cosine (degrees) | `cosd(0)` → `1` |
|
||||
| `tand(x)` | Tangent (degrees) | `tand(45)` → `1` |
|
||||
| `cotd(x)` | Cotangent (degrees) | `cotd(45)` → `1` |
|
||||
|
||||
### Hyperbolic Functions
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sinh(x)` | Hyperbolic sine | `sinh(1)` → `1.175...` |
|
||||
| `cosh(x)` | Hyperbolic cosine | `cosh(1)` → `1.543...` |
|
||||
| `tanh(x)` | Hyperbolic tangent | `tanh(1)` → `0.761...` |
|
||||
| `coth(x)` | Hyperbolic cotangent | `coth(2)` → `1.037...` |
|
||||
|
||||
### Inverse Trigonometric Functions
|
||||
|
||||
| Function | Aliases | Description | Example |
|
||||
|----------|---------|-------------|---------|
|
||||
| `arcsin(x)` | `asin(x)` | Inverse sine | `arcsin(0.5)` → `0.523...` |
|
||||
| `arccos(x)` | `acos(x)` | Inverse cosine | `arccos(0.5)` → `1.047...` |
|
||||
| `arctan(x)` | `atan(x)` | Inverse tangent | `arctan(1)` → `0.785...` |
|
||||
| `arccot(x)` | `acot(x)` | Inverse cotangent | `arccot(1)` → `0.785...` |
|
||||
|
||||
### Inverse Hyperbolic Functions
|
||||
|
||||
| Function | Aliases | Description | Example |
|
||||
|----------|---------|-------------|---------|
|
||||
| `arsinh(x)` | `asinh(x)`, `arcsinh(x)` | Inverse hyperbolic sine | `arsinh(1)` → `0.881...` |
|
||||
| `arcosh(x)` | `acosh(x)`, `arccosh(x)` | Inverse hyperbolic cosine | `arcosh(2)` → `1.316...` |
|
||||
| `artanh(x)` | `atanh(x)`, `arctanh(x)` | Inverse hyperbolic tangent | `artanh(0.5)` → `0.549...` |
|
||||
| `arcoth(x)` | `acoth(x)`, `arccoth(x)` | Inverse hyperbolic cotangent | `arcoth(2)` → `0.549...` |
|
||||
|
||||
### Logarithmic & Exponential Functions
|
||||
|
||||
| Function | Aliases | Description | Example |
|
||||
|----------|---------|-------------|---------|
|
||||
| `exp(x)` | - | Exponential (e^x) | `exp(2)` → `7.389...` |
|
||||
| `log(x)` | `ln(x)` | Natural logarithm (base e) | `log(e)` → `1` |
|
||||
| `log10(x)` | `lg(x)` | Logarithm base 10 | `log10(100)` → `2` |
|
||||
| `min(a, b, ...)` | Minimum value | `min({result}, 10)` |
|
||||
| `max(a, b, ...)` | Maximum value | `max({result}, 10)` |
|
||||
| `constant(name)` | PHP constant by name | `constant("PHP_INT_MAX")` |
|
||||
| `enum(name)` | PHP enum case by name | `enum("App\\Enum\\Status::Active")` |
|
||||
|
||||
---
|
||||
|
||||
## Constants
|
||||
|
||||
| Constant | Value | Description | Example |
|
||||
|----------|-------|-------------|---------|
|
||||
| `pi` | 3.14159265... | Ratio of circle circumference to diameter | `pi * r ^ 2` |
|
||||
| `e` | 2.71828182... | Euler's number | `e ^ x` |
|
||||
| `NAN` | Not a Number | Invalid mathematical result | - |
|
||||
| `INF` | Infinity | Positive infinity | - |
|
||||
ExpressionLanguage recognizes boolean and null literals:
|
||||
|
||||
| Constant | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| `true` | `true` | Boolean true |
|
||||
| `false` | `false` | Boolean false |
|
||||
| `null` | `null` | Null value |
|
||||
|
||||
---
|
||||
|
||||
@ -139,16 +176,12 @@ Or use string values: `'unknown'`, `'female'`, `'male'`
|
||||
|
||||
## Implicit Multiplication
|
||||
|
||||
The parser supports implicit multiplication (no explicit `*` operator needed):
|
||||
Implicit multiplication is not supported. Always use `*` between values:
|
||||
|
||||
| Expression | Parsed As | Result (x=2, y=3) |
|
||||
|------------|-----------|-------------------|
|
||||
| `2x` | `2 * x` | `4` |
|
||||
| `x sin(x)` | `x * sin(x)` | `1.818...` |
|
||||
| `2xy` | `2 * x * y` | `12` |
|
||||
| `x^2y` | `x^2 * y` | `12` |
|
||||
|
||||
**Note:** Implicit multiplication has the same precedence as explicit multiplication. `xy^2z` is parsed as `x*y^2*z`, NOT as `x*y^(2*z)`.
|
||||
| Expression | Use Instead |
|
||||
|------------|-------------|
|
||||
| `2x` | `2 * x` |
|
||||
| `{result}{factor}` | `{result} * {factor}` |
|
||||
|
||||
---
|
||||
|
||||
@ -165,13 +198,9 @@ $calculator = new CalculatorService();
|
||||
$result = $calculator->calculate("5 + 3 * 2");
|
||||
// Result: 11
|
||||
|
||||
// Using functions
|
||||
$result = $calculator->calculate("sqrt(16) + abs(-5)");
|
||||
// Result: 9
|
||||
|
||||
// Using constants
|
||||
$result = $calculator->calculate("2 * pi * r", ['r' => 5]);
|
||||
// Result: 31.415...
|
||||
// Using min/max
|
||||
$result = $calculator->calculate("max({result}, 10)", ['result' => 7]);
|
||||
// Result: 10
|
||||
```
|
||||
|
||||
### With Variables
|
||||
@ -190,7 +219,7 @@ $result = $calculator->calculate($formula, $variables);
|
||||
### BMI Calculation
|
||||
|
||||
```php
|
||||
$formula = "{weight} / ({height} ^ 2)";
|
||||
$formula = "{weight} / ({height} ** 2)";
|
||||
$variables = [
|
||||
'weight' => 70, // kg
|
||||
'height' => 1.75 // meters
|
||||
@ -217,8 +246,8 @@ $result = $calculator->calculate($formula, $variables);
|
||||
### Complex Formula
|
||||
|
||||
```php
|
||||
// Pythagorean theorem with rounding
|
||||
$formula = "round(sqrt({a} ^ 2 + {b} ^ 2))";
|
||||
// Pythagorean theorem
|
||||
$formula = "(({a} ** 2 + {b} ** 2) ** 0.5)";
|
||||
$variables = [
|
||||
'a' => 3,
|
||||
'b' => 4
|
||||
@ -304,6 +333,5 @@ Common errors:
|
||||
|
||||
## References
|
||||
|
||||
- [math-parser GitHub](https://github.com/mossadal/math-parser)
|
||||
- [math-parser Documentation](http://mossadal.github.io/math-parser/)
|
||||
- [Symfony ExpressionLanguage](https://symfony.com/doc/current/components/expression_language.html)
|
||||
- `app/Services/CalculatorService.php`
|
||||
@ -142,7 +142,136 @@ if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'
|
||||
if(requested('GLU'); test_delete('INS'):comment_insert('Duplicate insulin request removed'); nothing)
|
||||
```
|
||||
|
||||
## API Usage
|
||||
## API Endpoints
|
||||
|
||||
All endpoints live under `/api/rule` (singular) and accept JSON. Responses use the standard `{ status, message, data }` envelope. These endpoints require auth (bearer token).
|
||||
|
||||
### List Rules
|
||||
|
||||
```http
|
||||
GET /api/rule?EventCode=test_created&TestSiteID=12&search=glucose
|
||||
```
|
||||
|
||||
Query Params:
|
||||
|
||||
- `EventCode` (optional) filter by event code.
|
||||
- `TestSiteID` (optional) filter rules linked to a test site.
|
||||
- `search` (optional) partial match against `RuleName`.
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "fetch success",
|
||||
"data": [
|
||||
{
|
||||
"RuleID": 1,
|
||||
"RuleCode": "RULE_001",
|
||||
"RuleName": "Sex-based result",
|
||||
"EventCode": "test_created",
|
||||
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
||||
"ConditionExprCompiled": "{...}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get Rule
|
||||
|
||||
```http
|
||||
GET /api/rule/1
|
||||
```
|
||||
|
||||
Response includes `linkedTests`:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "fetch success",
|
||||
"data": {
|
||||
"RuleID": 1,
|
||||
"RuleCode": "RULE_001",
|
||||
"RuleName": "Sex-based result",
|
||||
"EventCode": "test_created",
|
||||
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
||||
"ConditionExprCompiled": "{...}",
|
||||
"linkedTests": [1, 2]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create Rule
|
||||
|
||||
```http
|
||||
POST /api/rule
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"RuleCode": "RULE_001",
|
||||
"RuleName": "Sex-based result",
|
||||
"EventCode": "test_created",
|
||||
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
||||
"ConditionExprCompiled": "<compiled JSON here>",
|
||||
"TestSiteIDs": [1, 2]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Rule created successfully",
|
||||
"data": {
|
||||
"RuleID": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Rule
|
||||
|
||||
```http
|
||||
PATCH /api/rule/1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"RuleName": "Sex-based result v2",
|
||||
"ConditionExpr": "if(sex('M'); result_set(0.7); result_set(0.6))",
|
||||
"ConditionExprCompiled": "<compiled JSON here>",
|
||||
"TestSiteIDs": [1, 3]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Rule updated successfully",
|
||||
"data": {
|
||||
"RuleID": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Rule
|
||||
|
||||
```http
|
||||
DELETE /api/rule/1
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Rule deleted successfully",
|
||||
"data": {
|
||||
"RuleID": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Compile DSL
|
||||
|
||||
@ -157,11 +286,26 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
The response contains `raw`, `compiled`, and `conditionExprCompiled` fields; store the JSON payload in `ConditionExprCompiled` before saving the rule.
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"raw": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
||||
"compiled": {
|
||||
"conditionExpr": "sex('M')",
|
||||
"then": ["result_set(0.5)"],
|
||||
"else": ["result_set(0.6)"]
|
||||
},
|
||||
"conditionExprCompiled": "{...}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Evaluate Expression (Validation)
|
||||
|
||||
This endpoint simply evaluates an expression against a runtime context. It does not compile DSL or persist the result.
|
||||
This endpoint evaluates an expression against a runtime context. It does not compile DSL or persist the result.
|
||||
|
||||
```http
|
||||
POST /api/rule/validate
|
||||
@ -177,19 +321,15 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
### Create Rule (example)
|
||||
|
||||
```http
|
||||
POST /api/rule
|
||||
Content-Type: application/json
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"RuleCode": "RULE_001",
|
||||
"RuleName": "Sex-based result",
|
||||
"EventCode": "test_created",
|
||||
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
||||
"ConditionExprCompiled": "<compiled JSON here>",
|
||||
"TestSiteIDs": [1, 2]
|
||||
"status": "success",
|
||||
"data": {
|
||||
"valid": true,
|
||||
"result": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -226,11 +226,11 @@ paths:
|
||||
responses:
|
||||
'201':
|
||||
description: User created
|
||||
/api/calc/{codeOrName}:
|
||||
/api/calc/testcode/{codeOrName}:
|
||||
post:
|
||||
tags:
|
||||
- Calculation
|
||||
summary: Evaluate a configured calculation by test code or name and return the numeric result only.
|
||||
summary: Evaluate a configured calculation by test code or name and return the raw result map.
|
||||
security: []
|
||||
parameters:
|
||||
- name: codeOrName
|
||||
@ -264,6 +264,81 @@ paths:
|
||||
IBIL: 2
|
||||
incomplete:
|
||||
value: {}
|
||||
/api/calc/testsite/{testSiteID}:
|
||||
post:
|
||||
tags:
|
||||
- Calculation
|
||||
summary: Evaluate a calculation defined for a test site and return a structured result.
|
||||
security: []
|
||||
parameters:
|
||||
- name: testSiteID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Identifier for the test site whose definition should be evaluated.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
description: Variable assignments required by the test site formula.
|
||||
additionalProperties:
|
||||
type: number
|
||||
example:
|
||||
result: 85
|
||||
gender: female
|
||||
age: 30
|
||||
responses:
|
||||
'200':
|
||||
description: Returns the calculated result, testSiteID, formula code, and echoed variables.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: number
|
||||
testSiteID:
|
||||
type: integer
|
||||
formula:
|
||||
type: string
|
||||
variables:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
examples:
|
||||
success:
|
||||
value:
|
||||
status: success
|
||||
data:
|
||||
result: 92.4
|
||||
testSiteID: 123
|
||||
formula: '{result} * {factor} + {age}'
|
||||
variables:
|
||||
result: 85
|
||||
gender: female
|
||||
age: 30
|
||||
'404':
|
||||
description: No calculation defined for the requested test site.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: failed
|
||||
message:
|
||||
type: string
|
||||
example: No calculation defined for this test site
|
||||
/api/contact:
|
||||
get:
|
||||
tags:
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/api/calc/{codeOrName}:
|
||||
/api/calc/testcode/{codeOrName}:
|
||||
post:
|
||||
tags: [Calculation]
|
||||
summary: Evaluate a configured calculation by test code or name and return the numeric result only.
|
||||
summary: Evaluate a configured calculation by test code or name and return the raw result map.
|
||||
security: []
|
||||
parameters:
|
||||
- name: codeOrName
|
||||
@ -35,3 +35,78 @@
|
||||
IBIL: 2.0
|
||||
incomplete:
|
||||
value: {}
|
||||
|
||||
/api/calc/testsite/{testSiteID}:
|
||||
post:
|
||||
tags: [Calculation]
|
||||
summary: Evaluate a calculation defined for a test site and return a structured result.
|
||||
security: []
|
||||
parameters:
|
||||
- name: testSiteID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Identifier for the test site whose definition should be evaluated.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
description: Variable assignments required by the test site formula.
|
||||
additionalProperties:
|
||||
type: number
|
||||
example:
|
||||
result: 85
|
||||
gender: "female"
|
||||
age: 30
|
||||
responses:
|
||||
'200':
|
||||
description: Returns the calculated result, testSiteID, formula code, and echoed variables.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: number
|
||||
testSiteID:
|
||||
type: integer
|
||||
formula:
|
||||
type: string
|
||||
variables:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
examples:
|
||||
success:
|
||||
value:
|
||||
status: success
|
||||
data:
|
||||
result: 92.4
|
||||
testSiteID: 123
|
||||
formula: "{result} * {factor} + {age}"
|
||||
variables:
|
||||
result: 85
|
||||
gender: female
|
||||
age: 30
|
||||
'404':
|
||||
description: No calculation defined for the requested test site.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: failed
|
||||
message:
|
||||
type: string
|
||||
example: No calculation defined for this test site
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user