feat: update calc endpoints and rule docs

This commit is contained in:
mahdahar 2026-03-17 16:50:57 +07:00
parent 4bb5496073
commit 6ece30302f
26 changed files with 947 additions and 686 deletions

Binary file not shown.

Binary file not shown.

View 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'

Binary file not shown.

3
.gitignore vendored
View File

@ -124,4 +124,5 @@ _modules/*
/results/ /results/
/phpunit*.xml /phpunit*.xml
/public/.htaccess /public/.htaccess

2
.serena/.gitignore vendored
View File

@ -1,2 +0,0 @@
/cache
/project.local.yml

View File

@ -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`

View File

@ -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.

View File

@ -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`.

View File

@ -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).

View File

@ -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 readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []

View File

@ -17,16 +17,16 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) {
$routes->get('dashboard', 'DashboardController::index'); $routes->get('dashboard', 'DashboardController::index');
$routes->get('sample', 'SampleController::index'); $routes->get('sample', 'SampleController::index');
// Results CRUD // Results CRUD
$routes->group('result', function ($routes) { $routes->group('result', function ($routes) {
$routes->get('/', 'ResultController::index'); $routes->get('/', 'ResultController::index');
$routes->get('(:num)', 'ResultController::show/$1'); $routes->get('(:num)', 'ResultController::show/$1');
$routes->patch('(:num)', 'ResultController::update/$1'); $routes->patch('(:num)', 'ResultController::update/$1');
$routes->delete('(:num)', 'ResultController::delete/$1'); $routes->delete('(:num)', 'ResultController::delete/$1');
}); });
// Reports // Reports
$routes->get('report/(:num)', 'ReportController::view/$1'); $routes->get('report/(:num)', 'ReportController::view/$1');
}); });
@ -58,7 +58,7 @@ $routes->group('api', function ($routes) {
$routes->post('/', 'Patient\PatientController::create'); $routes->post('/', 'Patient\PatientController::create');
$routes->get('(:num)', 'Patient\PatientController::show/$1'); $routes->get('(:num)', 'Patient\PatientController::show/$1');
$routes->delete('/', 'Patient\PatientController::delete'); $routes->delete('/', 'Patient\PatientController::delete');
$routes->patch('(:num)', 'Patient\PatientController::update/$1'); $routes->patch('(:num)', 'Patient\PatientController::update/$1');
$routes->get('check', 'Patient\PatientController::patientCheck'); $routes->get('check', 'Patient\PatientController::patientCheck');
}); });
@ -69,14 +69,14 @@ $routes->group('api', function ($routes) {
$routes->get('patient/(:num)', 'PatVisitController::showByPatient/$1'); $routes->get('patient/(:num)', 'PatVisitController::showByPatient/$1');
$routes->get('(:any)', 'PatVisitController::show/$1'); $routes->get('(:any)', 'PatVisitController::show/$1');
$routes->delete('/', 'PatVisitController::delete'); $routes->delete('/', 'PatVisitController::delete');
$routes->patch('(:any)', 'PatVisitController::update/$1'); $routes->patch('(:any)', 'PatVisitController::update/$1');
}); });
$routes->group('patvisitadt', function ($routes) { $routes->group('patvisitadt', function ($routes) {
$routes->get('visit/(:num)', 'PatVisitController::getADTByVisit/$1'); $routes->get('visit/(:num)', 'PatVisitController::getADTByVisit/$1');
$routes->get('(:num)', 'PatVisitController::showADT/$1'); $routes->get('(:num)', 'PatVisitController::showADT/$1');
$routes->post('/', 'PatVisitController::createADT'); $routes->post('/', 'PatVisitController::createADT');
$routes->patch('(:num)', 'PatVisitController::updateADT/$1'); $routes->patch('(:num)', 'PatVisitController::updateADT/$1');
$routes->delete('/', 'PatVisitController::deleteADT'); $routes->delete('/', 'PatVisitController::deleteADT');
}); });
@ -87,7 +87,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'LocationController::index'); $routes->get('/', 'LocationController::index');
$routes->get('(:num)', 'LocationController::show/$1'); $routes->get('(:num)', 'LocationController::show/$1');
$routes->post('/', 'LocationController::create'); $routes->post('/', 'LocationController::create');
$routes->patch('(:num)', 'LocationController::update/$1'); $routes->patch('(:num)', 'LocationController::update/$1');
$routes->delete('/', 'LocationController::delete'); $routes->delete('/', 'LocationController::delete');
}); });
@ -96,7 +96,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Contact\ContactController::index'); $routes->get('/', 'Contact\ContactController::index');
$routes->get('(:num)', 'Contact\ContactController::show/$1'); $routes->get('(:num)', 'Contact\ContactController::show/$1');
$routes->post('/', 'Contact\ContactController::create'); $routes->post('/', 'Contact\ContactController::create');
$routes->patch('(:num)', 'Contact\ContactController::update/$1'); $routes->patch('(:num)', 'Contact\ContactController::update/$1');
$routes->delete('/', 'Contact\ContactController::delete'); $routes->delete('/', 'Contact\ContactController::delete');
}); });
@ -104,7 +104,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Contact\OccupationController::index'); $routes->get('/', 'Contact\OccupationController::index');
$routes->get('(:num)', 'Contact\OccupationController::show/$1'); $routes->get('(:num)', 'Contact\OccupationController::show/$1');
$routes->post('/', 'Contact\OccupationController::create'); $routes->post('/', 'Contact\OccupationController::create');
$routes->patch('(:num)', 'Contact\OccupationController::update/$1'); $routes->patch('(:num)', 'Contact\OccupationController::update/$1');
//$routes->delete('/', 'Contact\OccupationController::delete'); //$routes->delete('/', 'Contact\OccupationController::delete');
}); });
@ -112,7 +112,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Contact\MedicalSpecialtyController::index'); $routes->get('/', 'Contact\MedicalSpecialtyController::index');
$routes->get('(:num)', 'Contact\MedicalSpecialtyController::show/$1'); $routes->get('(:num)', 'Contact\MedicalSpecialtyController::show/$1');
$routes->post('/', 'Contact\MedicalSpecialtyController::create'); $routes->post('/', 'Contact\MedicalSpecialtyController::create');
$routes->patch('(:num)', 'Contact\MedicalSpecialtyController::update/$1'); $routes->patch('(:num)', 'Contact\MedicalSpecialtyController::update/$1');
}); });
// Lib ValueSet (file-based) // Lib ValueSet (file-based)
@ -152,14 +152,15 @@ $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 // Counter
$routes->group('counter', function ($routes) { $routes->group('counter', function ($routes) {
$routes->get('/', 'CounterController::index'); $routes->get('/', 'CounterController::index');
$routes->get('(:num)', 'CounterController::show/$1'); $routes->get('(:num)', 'CounterController::show/$1');
$routes->post('/', 'CounterController::create'); $routes->post('/', 'CounterController::create');
$routes->patch('(:num)', 'CounterController::update/$1'); $routes->patch('(:num)', 'CounterController::update/$1');
$routes->delete('/', 'CounterController::delete'); $routes->delete('/', 'CounterController::delete');
}); });
@ -177,7 +178,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\AccountController::index'); $routes->get('/', 'Organization\AccountController::index');
$routes->get('(:num)', 'Organization\AccountController::show/$1'); $routes->get('(:num)', 'Organization\AccountController::show/$1');
$routes->post('/', 'Organization\AccountController::create'); $routes->post('/', 'Organization\AccountController::create');
$routes->patch('(:num)', 'Organization\AccountController::update/$1'); $routes->patch('(:num)', 'Organization\AccountController::update/$1');
$routes->delete('/', 'Organization\AccountController::delete'); $routes->delete('/', 'Organization\AccountController::delete');
}); });
@ -186,7 +187,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\SiteController::index'); $routes->get('/', 'Organization\SiteController::index');
$routes->get('(:num)', 'Organization\SiteController::show/$1'); $routes->get('(:num)', 'Organization\SiteController::show/$1');
$routes->post('/', 'Organization\SiteController::create'); $routes->post('/', 'Organization\SiteController::create');
$routes->patch('(:num)', 'Organization\SiteController::update/$1'); $routes->patch('(:num)', 'Organization\SiteController::update/$1');
$routes->delete('/', 'Organization\SiteController::delete'); $routes->delete('/', 'Organization\SiteController::delete');
}); });
@ -195,7 +196,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\DisciplineController::index'); $routes->get('/', 'Organization\DisciplineController::index');
$routes->get('(:num)', 'Organization\DisciplineController::show/$1'); $routes->get('(:num)', 'Organization\DisciplineController::show/$1');
$routes->post('/', 'Organization\DisciplineController::create'); $routes->post('/', 'Organization\DisciplineController::create');
$routes->patch('(:num)', 'Organization\DisciplineController::update/$1'); $routes->patch('(:num)', 'Organization\DisciplineController::update/$1');
$routes->delete('/', 'Organization\DisciplineController::delete'); $routes->delete('/', 'Organization\DisciplineController::delete');
}); });
@ -204,7 +205,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\DepartmentController::index'); $routes->get('/', 'Organization\DepartmentController::index');
$routes->get('(:num)', 'Organization\DepartmentController::show/$1'); $routes->get('(:num)', 'Organization\DepartmentController::show/$1');
$routes->post('/', 'Organization\DepartmentController::create'); $routes->post('/', 'Organization\DepartmentController::create');
$routes->patch('(:num)', 'Organization\DepartmentController::update/$1'); $routes->patch('(:num)', 'Organization\DepartmentController::update/$1');
$routes->delete('/', 'Organization\DepartmentController::delete'); $routes->delete('/', 'Organization\DepartmentController::delete');
}); });
@ -213,7 +214,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\WorkstationController::index'); $routes->get('/', 'Organization\WorkstationController::index');
$routes->get('(:num)', 'Organization\WorkstationController::show/$1'); $routes->get('(:num)', 'Organization\WorkstationController::show/$1');
$routes->post('/', 'Organization\WorkstationController::create'); $routes->post('/', 'Organization\WorkstationController::create');
$routes->patch('(:num)', 'Organization\WorkstationController::update/$1'); $routes->patch('(:num)', 'Organization\WorkstationController::update/$1');
$routes->delete('/', 'Organization\WorkstationController::delete'); $routes->delete('/', 'Organization\WorkstationController::delete');
}); });
@ -222,7 +223,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\HostAppController::index'); $routes->get('/', 'Organization\HostAppController::index');
$routes->get('(:any)', 'Organization\HostAppController::show/$1'); $routes->get('(:any)', 'Organization\HostAppController::show/$1');
$routes->post('/', 'Organization\HostAppController::create'); $routes->post('/', 'Organization\HostAppController::create');
$routes->patch('(:any)', 'Organization\HostAppController::update/$1'); $routes->patch('(:any)', 'Organization\HostAppController::update/$1');
$routes->delete('/', 'Organization\HostAppController::delete'); $routes->delete('/', 'Organization\HostAppController::delete');
}); });
@ -231,7 +232,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\HostComParaController::index'); $routes->get('/', 'Organization\HostComParaController::index');
$routes->get('(:any)', 'Organization\HostComParaController::show/$1'); $routes->get('(:any)', 'Organization\HostComParaController::show/$1');
$routes->post('/', 'Organization\HostComParaController::create'); $routes->post('/', 'Organization\HostComParaController::create');
$routes->patch('(:any)', 'Organization\HostComParaController::update/$1'); $routes->patch('(:any)', 'Organization\HostComParaController::update/$1');
$routes->delete('/', 'Organization\HostComParaController::delete'); $routes->delete('/', 'Organization\HostComParaController::delete');
}); });
@ -240,7 +241,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Organization\CodingSysController::index'); $routes->get('/', 'Organization\CodingSysController::index');
$routes->get('(:num)', 'Organization\CodingSysController::show/$1'); $routes->get('(:num)', 'Organization\CodingSysController::show/$1');
$routes->post('/', 'Organization\CodingSysController::create'); $routes->post('/', 'Organization\CodingSysController::create');
$routes->patch('(:num)', 'Organization\CodingSysController::update/$1'); $routes->patch('(:num)', 'Organization\CodingSysController::update/$1');
$routes->delete('/', 'Organization\CodingSysController::delete'); $routes->delete('/', 'Organization\CodingSysController::delete');
}); });
}); });
@ -250,18 +251,18 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Infrastructure\EquipmentListController::index'); $routes->get('/', 'Infrastructure\EquipmentListController::index');
$routes->get('(:num)', 'Infrastructure\EquipmentListController::show/$1'); $routes->get('(:num)', 'Infrastructure\EquipmentListController::show/$1');
$routes->post('/', 'Infrastructure\EquipmentListController::create'); $routes->post('/', 'Infrastructure\EquipmentListController::create');
$routes->patch('(:num)', 'Infrastructure\EquipmentListController::update/$1'); $routes->patch('(:num)', 'Infrastructure\EquipmentListController::update/$1');
$routes->delete('/', 'Infrastructure\EquipmentListController::delete'); $routes->delete('/', 'Infrastructure\EquipmentListController::delete');
}); });
// Users // Users
$routes->group('user', function ($routes) { $routes->group('user', function ($routes) {
$routes->get('/', 'User\UserController::index'); $routes->get('/', 'User\UserController::index');
$routes->get('(:num)', 'User\UserController::show/$1'); $routes->get('(:num)', 'User\UserController::show/$1');
$routes->post('/', 'User\UserController::create'); $routes->post('/', 'User\UserController::create');
$routes->patch('(:num)', 'User\UserController::update/$1'); $routes->patch('(:num)', 'User\UserController::update/$1');
$routes->delete('(:num)', 'User\UserController::delete/$1'); $routes->delete('(:num)', 'User\UserController::delete/$1');
}); });
// Specimen // Specimen
$routes->group('specimen', function ($routes) { $routes->group('specimen', function ($routes) {
@ -270,54 +271,54 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Specimen\ContainerDefController::index'); $routes->get('/', 'Specimen\ContainerDefController::index');
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1'); $routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
$routes->post('/', 'Specimen\ContainerDefController::create'); $routes->post('/', 'Specimen\ContainerDefController::create');
$routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1'); $routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1');
}); });
$routes->group('containerdef', function ($routes) { $routes->group('containerdef', function ($routes) {
$routes->get('/', 'Specimen\ContainerDefController::index'); $routes->get('/', 'Specimen\ContainerDefController::index');
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1'); $routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
$routes->post('/', 'Specimen\ContainerDefController::create'); $routes->post('/', 'Specimen\ContainerDefController::create');
$routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1'); $routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1');
}); });
$routes->group('prep', function ($routes) { $routes->group('prep', function ($routes) {
$routes->get('/', 'Specimen\SpecimenPrepController::index'); $routes->get('/', 'Specimen\SpecimenPrepController::index');
$routes->get('(:num)', 'Specimen\SpecimenPrepController::show/$1'); $routes->get('(:num)', 'Specimen\SpecimenPrepController::show/$1');
$routes->post('/', 'Specimen\SpecimenPrepController::create'); $routes->post('/', 'Specimen\SpecimenPrepController::create');
$routes->patch('(:num)', 'Specimen\SpecimenPrepController::update/$1'); $routes->patch('(:num)', 'Specimen\SpecimenPrepController::update/$1');
}); });
$routes->group('status', function ($routes) { $routes->group('status', function ($routes) {
$routes->get('/', 'Specimen\SpecimenStatusController::index'); $routes->get('/', 'Specimen\SpecimenStatusController::index');
$routes->get('(:num)', 'Specimen\SpecimenStatusController::show/$1'); $routes->get('(:num)', 'Specimen\SpecimenStatusController::show/$1');
$routes->post('/', 'Specimen\SpecimenStatusController::create'); $routes->post('/', 'Specimen\SpecimenStatusController::create');
$routes->patch('(:num)', 'Specimen\SpecimenStatusController::update/$1'); $routes->patch('(:num)', 'Specimen\SpecimenStatusController::update/$1');
}); });
$routes->group('collection', function ($routes) { $routes->group('collection', function ($routes) {
$routes->get('/', 'Specimen\SpecimenCollectionController::index'); $routes->get('/', 'Specimen\SpecimenCollectionController::index');
$routes->get('(:num)', 'Specimen\SpecimenCollectionController::show/$1'); $routes->get('(:num)', 'Specimen\SpecimenCollectionController::show/$1');
$routes->post('/', 'Specimen\SpecimenCollectionController::create'); $routes->post('/', 'Specimen\SpecimenCollectionController::create');
$routes->patch('(:num)', 'Specimen\SpecimenCollectionController::update/$1'); $routes->patch('(:num)', 'Specimen\SpecimenCollectionController::update/$1');
}); });
$routes->get('/', 'Specimen\SpecimenController::index'); $routes->get('/', 'Specimen\SpecimenController::index');
$routes->get('(:num)', 'Specimen\SpecimenController::show/$1'); $routes->get('(:num)', 'Specimen\SpecimenController::show/$1');
$routes->post('/', 'Specimen\SpecimenController::create'); $routes->post('/', 'Specimen\SpecimenController::create');
$routes->patch('(:num)', 'Specimen\SpecimenController::update/$1'); $routes->patch('(:num)', 'Specimen\SpecimenController::update/$1');
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1'); $routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
}); });
// Test Mapping // Test
$routes->group('test', function ($routes) { $routes->group('test', function ($routes) {
$routes->get('/', 'Test\TestsController::index'); $routes->get('/', 'Test\TestsController::index');
$routes->get('(:num)', 'Test\TestsController::show/$1'); $routes->get('(:num)', 'Test\TestsController::show/$1');
$routes->post('/', 'Test\TestsController::create'); $routes->post('/', 'Test\TestsController::create');
$routes->patch('(:num)', 'Test\TestsController::update/$1'); $routes->patch('(:num)', 'Test\TestsController::update/$1');
$routes->group('testmap', function ($routes) { $routes->group('testmap', function ($routes) {
$routes->get('/', 'Test\TestMapController::index'); $routes->get('/', 'Test\TestMapController::index');
$routes->get('(:num)', 'Test\TestMapController::show/$1'); $routes->get('(:num)', 'Test\TestMapController::show/$1');
$routes->post('/', 'Test\TestMapController::create'); $routes->post('/', 'Test\TestMapController::create');
$routes->patch('(:num)', 'Test\TestMapController::update/$1'); $routes->patch('(:num)', 'Test\TestMapController::update/$1');
$routes->delete('/', 'Test\TestMapController::delete'); $routes->delete('/', 'Test\TestMapController::delete');
// Filter routes // Filter routes
@ -328,7 +329,7 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'Test\TestMapDetailController::index'); $routes->get('/', 'Test\TestMapDetailController::index');
$routes->get('(:num)', 'Test\TestMapDetailController::show/$1'); $routes->get('(:num)', 'Test\TestMapDetailController::show/$1');
$routes->post('/', 'Test\TestMapDetailController::create'); $routes->post('/', 'Test\TestMapDetailController::create');
$routes->patch('(:num)', 'Test\TestMapDetailController::update/$1'); $routes->patch('(:num)', 'Test\TestMapDetailController::update/$1');
$routes->delete('/', 'Test\TestMapDetailController::delete'); $routes->delete('/', 'Test\TestMapDetailController::delete');
$routes->get('by-testmap/(:num)', 'Test\TestMapDetailController::showByTestMap/$1'); $routes->get('by-testmap/(:num)', 'Test\TestMapDetailController::showByTestMap/$1');
$routes->post('batch', 'Test\TestMapDetailController::batchCreate'); $routes->post('batch', 'Test\TestMapDetailController::batchCreate');
@ -343,13 +344,13 @@ $routes->group('api', function ($routes) {
$routes->get('/', 'OrderTestController::index'); $routes->get('/', 'OrderTestController::index');
$routes->get('(:any)', 'OrderTestController::show/$1'); $routes->get('(:any)', 'OrderTestController::show/$1');
$routes->post('/', 'OrderTestController::create'); $routes->post('/', 'OrderTestController::create');
$routes->patch('(:any)', 'OrderTestController::update/$1'); $routes->patch('(:any)', 'OrderTestController::update/$1');
$routes->delete('/', 'OrderTestController::delete'); $routes->delete('/', 'OrderTestController::delete');
$routes->post('status', 'OrderTestController::updateStatus'); $routes->post('status', 'OrderTestController::updateStatus');
}); });
// Rules // Rules
$routes->group('rule', function ($routes) { $routes->group('rule', ['filter' => 'auth'], function ($routes) {
$routes->get('/', 'Rule\RuleController::index'); $routes->get('/', 'Rule\RuleController::index');
$routes->get('(:num)', 'Rule\RuleController::show/$1'); $routes->get('(:num)', 'Rule\RuleController::show/$1');
$routes->post('/', 'Rule\RuleController::create'); $routes->post('/', 'Rule\RuleController::create');
@ -358,21 +359,22 @@ $routes->group('api', function ($routes) {
$routes->post('validate', 'Rule\RuleController::validateExpr'); $routes->post('validate', 'Rule\RuleController::validateExpr');
$routes->post('compile', 'Rule\RuleController::compile'); $routes->post('compile', 'Rule\RuleController::compile');
}); });
// Demo/Test Routes (No Auth) // Demo/Test Routes (No Auth)
$routes->group('api/demo', function ($routes) { $routes->group('api/demo', function ($routes) {
$routes->post('order', 'Test\DemoOrderController::createDemoOrder'); $routes->post('order', 'Test\DemoOrderController::createDemoOrder');
$routes->get('order', 'Test\DemoOrderController::listDemoOrders'); $routes->get('order', 'Test\DemoOrderController::listDemoOrders');
}); });
// Edge API - Integration with tiny-edge // Edge API - Integration with tiny-edge
$routes->group('edge', function ($routes) { $routes->group('edge', function ($routes) {
$routes->post('result', 'EdgeController::results'); $routes->post('result', 'EdgeController::results');
$routes->get('order', 'EdgeController::orders'); $routes->get('order', 'EdgeController::orders');
$routes->post('order/(:num)/ack', 'EdgeController::ack/$1'); $routes->post('order/(:num)/ack', 'EdgeController::ack/$1');
$routes->post('status', 'EdgeController::status'); $routes->post('status', 'EdgeController::status');
}); });
}); });
// Khusus // Khusus
/* /*

View File

@ -107,9 +107,9 @@ class CalculatorController extends Controller
} }
} }
/** /**
* POST api/calculate/test-site/{testSiteID} * POST api/calc/testsite/{testSiteID}
* Calculate using TestDefCal definition * Calculate using TestDefCal definition
* *
* Request: { * Request: {
* "result": 85, * "result": 85,
@ -150,9 +150,9 @@ 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. * Evaluate a configured calculation by its code or name and return only the result map.
*/ */
public function calculateByCodeOrName($codeOrName): ResponseInterface public function calculateByCodeOrName($codeOrName): ResponseInterface
{ {

View File

@ -9,7 +9,7 @@ class CreateTestDefinitions extends Migration {
$this->forge->addField([ $this->forge->addField([
'TestSiteID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true], 'TestSiteID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
'SiteID' => ['type' => 'INT', 'null' => false], '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], 'TestSiteName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => false],
'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false], 'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
'Description' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true], 'Description' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],

View File

@ -85,33 +85,10 @@ class CreateSpecimens extends Migration {
$this->forge->addKey('SpcPrpID', true); $this->forge->addKey('SpcPrpID', true);
$this->forge->createTable('specimenprep'); $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() { public function down() {
$this->forge->dropTable('specimenlog'); $this->forge->dropTable('specimenprep');
$this->forge->dropTable('specimenprep');
$this->forge->dropTable('specimencollection'); $this->forge->dropTable('specimencollection');
$this->forge->dropTable('specimenstatus'); $this->forge->dropTable('specimenstatus');
$this->forge->dropTable('specimen'); $this->forge->dropTable('specimen');

View File

@ -95,33 +95,10 @@ class CreatePatientCore extends Migration {
$this->forge->addKey('PatRelID', true); $this->forge->addKey('PatRelID', true);
$this->forge->createTable('patrelation'); $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() { public function down() {
$this->forge->dropTable('patreglog'); $this->forge->dropTable('patrelation');
$this->forge->dropTable('patrelation');
$this->forge->dropTable('patidt'); $this->forge->dropTable('patidt');
$this->forge->dropTable('patcom'); $this->forge->dropTable('patcom');
$this->forge->dropTable('patatt'); $this->forge->dropTable('patatt');

View File

@ -52,34 +52,10 @@ class CreatePatientVisits extends Migration {
$this->forge->addKey('PVADTID', true); $this->forge->addKey('PVADTID', true);
$this->forge->createTable('patvisitadt'); $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() { public function down() {
$this->forge->dropTable('patvisitlog'); $this->forge->dropTable('patvisitadt');
$this->forge->dropTable('patvisitadt');
$this->forge->dropTable('patdiag'); $this->forge->dropTable('patdiag');
$this->forge->dropTable('patvisit'); $this->forge->dropTable('patvisit');
} }

View File

@ -2,8 +2,9 @@
namespace App\Database\Seeds; namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use App\Libraries\ValueSet; use App\Libraries\ValueSet;
use App\Services\RuleExpressionService;
class TestSeeder extends Seeder class TestSeeder extends Seeder
{ {
@ -189,22 +190,22 @@ class TestSeeder extends Seeder
$tIDs['DBIL'] = $this->db->insertID(); $tIDs['DBIL'] = $this->db->insertID();
// CALC: Chemistry Calculated Tests // CALC: Chemistry Calculated Tests
$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"]; $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); $this->db->table('testdefsite')->insert($data);
$tIDs['EGFR'] = $this->db->insertID(); $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); $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"]; $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); $this->db->table('testdefsite')->insert($data);
$tIDs['LDLCALC'] = $this->db->insertID(); $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); $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"]; $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); $this->db->table('testdefsite')->insert($data);
$tIDs['IBIL'] = $this->db->insertID(); $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); $this->db->table('testdefcal')->insert($data);
// CALC dependencies are grouped via testdefgrp // CALC dependencies are grouped via testdefgrp
@ -284,13 +285,25 @@ class TestSeeder extends Seeder
$this->db->table('testdefsite')->insert($data); $this->db->table('testdefsite')->insert($data);
$tIDs['WEIGHT'] = $this->db->insertID(); $tIDs['WEIGHT'] = $this->db->insertID();
$data = ['SiteID' => '1', 'TestSiteCode' => 'AGE', 'TestSiteName' => 'Age', 'TestType' => 'PARAM', 'Description' => 'Usia', 'SeqScr' => '30', 'SeqRpt' => '30', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'years', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; $data = ['SiteID' => '1', 'TestSiteCode' => 'AGE', 'TestSiteName' => 'Age', 'TestType' => 'PARAM', 'Description' => 'Usia', 'SeqScr' => '30', 'SeqRpt' => '30', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'years', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
$this->db->table('testdefsite')->insert($data); $this->db->table('testdefsite')->insert($data);
$tIDs['AGE'] = $this->db->insertID(); $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"]; $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); $this->db->table('testdefsite')->insert($data);
$tIDs['SYSTL'] = $this->db->insertID(); $tIDs['SYSTL'] = $this->db->insertID();
$data = ['SiteID' => '1', 'TestSiteCode' => 'DIASTL', 'TestSiteName' => 'Diastolic BP', 'TestType' => 'PARAM', 'Description' => 'Tekanan Darah Diastolik', 'SeqScr' => '50', 'SeqRpt' => '50', '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"]; $data = ['SiteID' => '1', 'TestSiteCode' => 'DIASTL', 'TestSiteName' => 'Diastolic BP', 'TestType' => 'PARAM', 'Description' => 'Tekanan Darah Diastolik', 'SeqScr' => '50', 'SeqRpt' => '50', '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); $this->db->table('testdefsite')->insert($data);
@ -302,14 +315,71 @@ 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"]; $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); $this->db->table('testdefsite')->insert($data);
$tIDs['BMI'] = $this->db->insertID(); $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('testdefcal')->insert($data);
$this->db->table('testdefgrp')->insertBatch([ $this->db->table('testdefgrp')->insertBatch([
['TestSiteID' => $tIDs['BMI'], 'Member' => $tIDs['WEIGHT'], 'CreateDate' => "$now"], ['TestSiteID' => $tIDs['BMI'], 'Member' => $tIDs['WEIGHT'], 'CreateDate' => "$now"],
['TestSiteID' => $tIDs['BMI'], 'Member' => $tIDs['HEIGHT'], 'CreateDate' => "$now"], ['TestSiteID' => $tIDs['BMI'], 'Member' => $tIDs['HEIGHT'], 'CreateDate' => "$now"],
['TestSiteID' => $tIDs['EGFR'], 'Member' => $tIDs['AGE'], '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 // TEST MAP - Specimen Mapping

View File

@ -1,14 +1,11 @@
<?php <?php
namespace App\Services; namespace App\Services;
use MathParser\StdMathParser; use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use MathParser\Interpreting\Evaluator;
use MathParser\Exceptions\MathParserException; class CalculatorService {
protected ExpressionLanguage $language;
class CalculatorService {
protected StdMathParser $parser;
protected Evaluator $evaluator;
/** /**
* Gender mapping for calculations * Gender mapping for calculations
@ -23,10 +20,9 @@ class CalculatorService {
'2' => 2, '2' => 2,
]; ];
public function __construct() { public function __construct() {
$this->parser = new StdMathParser(); $this->language = new ExpressionLanguage();
$this->evaluator = new Evaluator(); }
}
/** /**
* Calculate formula with variables * Calculate formula with variables
@ -37,26 +33,19 @@ class CalculatorService {
* @throws \Exception * @throws \Exception
*/ */
public function calculate(string $formula, array $variables = []): ?float { public function calculate(string $formula, array $variables = []): ?float {
try { try {
$normalizedFormula = $this->normalizeFormulaVariables($formula, $variables); $normalizedFormula = $this->normalizeFormulaVariables($formula, $variables);
$expression = $this->prepareExpression($normalizedFormula, $variables); [$expression, $context] = $this->prepareExpression($normalizedFormula, $variables);
// Parse the expression $result = $this->language->evaluate($expression, $context);
$ast = $this->parser->parse($expression);
return (float) $result;
// Evaluate } catch (\Throwable $e) {
$result = $ast->accept($this->evaluator); log_message('error', 'Calculator error: ' . $e->getMessage() . ' | Formula: ' . $formula);
throw new \Exception('Invalid formula: ' . $e->getMessage());
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) {
log_message('error', 'Calculator error: ' . $e->getMessage() . ' | Formula: ' . $formula);
throw $e;
}
}
/** /**
* Validate formula syntax * Validate formula syntax
* *
@ -64,15 +53,18 @@ class CalculatorService {
* @return array ['valid' => bool, 'error' => string|null] * @return array ['valid' => bool, 'error' => string|null]
*/ */
public function validate(string $formula): array { public function validate(string $formula): array {
try { $formula = $this->normalizeFormulaVariables($formula, []);
// Replace placeholders with dummy values for validation $variables = array_fill_keys($this->extractVariables($formula), 1);
$testExpression = preg_replace('/\{([^}]+)\}/', '1', $formula);
$this->parser->parse($testExpression); try {
return ['valid' => true, 'error' => null]; [$expression, $context] = $this->prepareExpression($formula, $variables);
} catch (MathParserException $e) { $this->language->evaluate($expression, $context);
return ['valid' => false, 'error' => $e->getMessage()];
} return ['valid' => true, 'error' => null];
} } catch (\Throwable $e) {
return ['valid' => false, 'error' => $e->getMessage()];
}
}
/** /**
* Extract variable names from formula * Extract variable names from formula
@ -88,32 +80,50 @@ class CalculatorService {
/** /**
* Prepare expression by replacing placeholders with values * 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; $expression = $formula;
$context = [];
foreach ($variables as $key => $value) { $usedNames = [];
$placeholder = '{' . $key . '}';
foreach ($variables as $key => $value) {
// Handle gender specially $placeholder = '{' . $key . '}';
if ($key === 'gender') {
$value = $this->normalizeGender($value); if ($key === 'gender') {
} $value = $this->normalizeGender($value);
}
// Ensure numeric value
if (!is_numeric($value)) { if (!is_numeric($value)) {
throw new \Exception("Variable '{$key}' must be numeric, got: " . var_export($value, true)); 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;
// Check for unreplaced placeholders
if (preg_match('/\{([^}]+)\}/', $expression, $unreplaced)) { $expression = str_replace($placeholder, $variableName, $expression);
throw new \Exception("Missing variable value for: {$unreplaced[1]}"); }
}
if (preg_match('/\{([^}]+)\}/', $expression, $unreplaced)) {
return $expression; throw new \Exception("Missing variable value for: {$unreplaced[1]}");
} }
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;
}
/** /**
* Normalize formulas that reference raw variable names instead of placeholders. * Normalize formulas that reference raw variable names instead of placeholders.

View File

@ -12,9 +12,8 @@
"require": { "require": {
"php": "^8.1", "php": "^8.1",
"codeigniter4/framework": "^4.0", "codeigniter4/framework": "^4.0",
"firebase/php-jwt": "^6.11", "firebase/php-jwt": "^6.11",
"mossadal/math-parser": "^1.3", "symfony/expression-language": "^7.0"
"symfony/expression-language": "^7.0"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.24", "fakerphp/faker": "^1.24",

53
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b111968eaeab80698adb5ca0eaeeb8c1", "content-hash": "ad96435c829500be9b0bb553558894ea",
"packages": [ "packages": [
{ {
"name": "codeigniter4/framework", "name": "codeigniter4/framework",
@ -204,57 +204,6 @@
], ],
"time": "2025-05-06T19:29:36+00:00" "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", "name": "psr/cache",
"version": "3.0.0", "version": "3.0.0",

194
docs/audit-logging.md Normal file
View 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.

View File

@ -2,7 +2,68 @@
## Overview ## 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` | | `-` | Subtraction | `10 - 4` | `6` |
| `*` | Multiplication | `6 * 7` | `42` | | `*` | Multiplication | `6 * 7` | `42` |
| `/` | Division | `20 / 4` | `5` | | `/` | Division | `20 / 4` | `5` |
| `^` | Exponentiation (power) | `2 ^ 3` | `8` | | `%` | Modulo | `20 % 6` | `2` |
| `!` | Factorial | `5!` | `120` | | `**` | Exponentiation (power) | `2 ** 3` | `8` |
| `!!` | Semi-factorial (double factorial) | `5!!` | `15` |
### 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 ### Parentheses
@ -29,84 +115,35 @@ Use parentheses to control operation precedence:
2 + 3 * 4 // Result: 14 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 | | Function | Description | Example |
|----------|-------------|---------| |----------|-------------|---------|
| `sqrt(x)` | Square root | `sqrt(16)``4` | | `min(a, b, ...)` | Minimum value | `min({result}, 10)` |
| `round(x)` | Round to nearest integer | `round(3.7)``4` | | `max(a, b, ...)` | Maximum value | `max({result}, 10)` |
| `ceil(x)` | Round up to integer | `ceil(3.2)``4` | | `constant(name)` | PHP constant by name | `constant("PHP_INT_MAX")` |
| `floor(x)` | Round down to integer | `floor(3.9)``3` | | `enum(name)` | PHP enum case by name | `enum("App\\Enum\\Status::Active")` |
| `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` |
--- ---
## Constants ## Constants
| Constant | Value | Description | Example | ExpressionLanguage recognizes boolean and null literals:
|----------|-------|-------------|---------|
| `pi` | 3.14159265... | Ratio of circle circumference to diameter | `pi * r ^ 2` | | Constant | Value | Description |
| `e` | 2.71828182... | Euler's number | `e ^ x` | |----------|-------|-------------|
| `NAN` | Not a Number | Invalid mathematical result | - | | `true` | `true` | Boolean true |
| `INF` | Infinity | Positive infinity | - | | `false` | `false` | Boolean false |
| `null` | `null` | Null value |
--- ---
@ -139,16 +176,12 @@ Or use string values: `'unknown'`, `'female'`, `'male'`
## Implicit Multiplication ## 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) | | Expression | Use Instead |
|------------|-----------|-------------------| |------------|-------------|
| `2x` | `2 * x` | `4` | | `2x` | `2 * x` |
| `x sin(x)` | `x * sin(x)` | `1.818...` | | `{result}{factor}` | `{result} * {factor}` |
| `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)`.
--- ---
@ -165,13 +198,9 @@ $calculator = new CalculatorService();
$result = $calculator->calculate("5 + 3 * 2"); $result = $calculator->calculate("5 + 3 * 2");
// Result: 11 // Result: 11
// Using functions // Using min/max
$result = $calculator->calculate("sqrt(16) + abs(-5)"); $result = $calculator->calculate("max({result}, 10)", ['result' => 7]);
// Result: 9 // Result: 10
// Using constants
$result = $calculator->calculate("2 * pi * r", ['r' => 5]);
// Result: 31.415...
``` ```
### With Variables ### With Variables
@ -190,7 +219,7 @@ $result = $calculator->calculate($formula, $variables);
### BMI Calculation ### BMI Calculation
```php ```php
$formula = "{weight} / ({height} ^ 2)"; $formula = "{weight} / ({height} ** 2)";
$variables = [ $variables = [
'weight' => 70, // kg 'weight' => 70, // kg
'height' => 1.75 // meters 'height' => 1.75 // meters
@ -217,8 +246,8 @@ $result = $calculator->calculate($formula, $variables);
### Complex Formula ### Complex Formula
```php ```php
// Pythagorean theorem with rounding // Pythagorean theorem
$formula = "round(sqrt({a} ^ 2 + {b} ^ 2))"; $formula = "(({a} ** 2 + {b} ** 2) ** 0.5)";
$variables = [ $variables = [
'a' => 3, 'a' => 3,
'b' => 4 'b' => 4
@ -304,6 +333,5 @@ Common errors:
## References ## References
- [math-parser GitHub](https://github.com/mossadal/math-parser) - [Symfony ExpressionLanguage](https://symfony.com/doc/current/components/expression_language.html)
- [math-parser Documentation](http://mossadal.github.io/math-parser/)
- `app/Services/CalculatorService.php` - `app/Services/CalculatorService.php`

View File

@ -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) 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 ### 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) ### 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 ```http
POST /api/rule/validate POST /api/rule/validate
@ -177,19 +321,15 @@ Content-Type: application/json
} }
``` ```
### Create Rule (example) Response:
```http
POST /api/rule
Content-Type: application/json
```json
{ {
"RuleCode": "RULE_001", "status": "success",
"RuleName": "Sex-based result", "data": {
"EventCode": "test_created", "valid": true,
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))", "result": true
"ConditionExprCompiled": "<compiled JSON here>", }
"TestSiteIDs": [1, 2]
} }
``` ```

View File

@ -226,11 +226,11 @@ paths:
responses: responses:
'201': '201':
description: User created description: User created
/api/calc/{codeOrName}: /api/calc/testcode/{codeOrName}:
post: post:
tags: tags:
- Calculation - 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: [] security: []
parameters: parameters:
- name: codeOrName - name: codeOrName
@ -264,6 +264,81 @@ paths:
IBIL: 2 IBIL: 2
incomplete: incomplete:
value: {} 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: /api/contact:
get: get:
tags: tags:

View File

@ -1,37 +1,112 @@
/api/calc/{codeOrName}: /api/calc/testcode/{codeOrName}:
post: post:
tags: [Calculation] 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: [] security: []
parameters: parameters:
- name: codeOrName - name: codeOrName
in: path in: path
required: true required: true
schema: schema:
type: string type: string
description: TestSiteCode or TestSiteName of the calculated test (case-insensitive). description: TestSiteCode or TestSiteName of the calculated test (case-insensitive).
requestBody: requestBody:
required: true required: true
content: content:
application/json: application/json:
schema: schema:
type: object type: object
description: Key-value pairs where keys match member tests used in the formula. description: Key-value pairs where keys match member tests used in the formula.
additionalProperties: additionalProperties:
type: number type: number
example: example:
TBIL: 5 TBIL: 5
DBIL: 3 DBIL: 3
responses: responses:
'200': '200':
description: Returns a single key/value pair with the canonical TestSiteCode or an empty object when the calculation is incomplete or missing. description: Returns a single key/value pair with the canonical TestSiteCode or an empty object when the calculation is incomplete or missing.
content: content:
application/json: application/json:
schema: schema:
type: object type: object
examples: examples:
success: success:
value: value:
IBIL: 2.0 IBIL: 2.0
incomplete: incomplete:
value: {} 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