diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.serena/.gitignore b/.serena/.gitignore index 2e510af..32a4ef9 100644 --- a/.serena/.gitignore +++ b/.serena/.gitignore @@ -1,2 +1,2 @@ -/cache -/project.local.yml +/cache +/project.local.yml diff --git a/.serena/memories/clqms01-be/suggested_commands.md b/.serena/memories/clqms01-be/suggested_commands.md index cf66ddc..768681b 100644 --- a/.serena/memories/clqms01-be/suggested_commands.md +++ b/.serena/memories/clqms01-be/suggested_commands.md @@ -1,14 +1,14 @@ -Essential commands for CLQMS development (run from repo root on Windows PowerShell): - -• `composer install` – install PHP dependencies before running CodeIgniter or tests. -• `npm install` – sync `package-lock.json` for tooling such as API docs bundler. -• `./vendor/bin/phpunit` – run entire PHPUnit suite (or target files via `--filter`). -• `php spark test --filter ::` – focused test run when you know the class/method. -• `php spark migrate` / `php spark migrate:rollback` – apply or roll back database migrations. -• `php spark serve` – lightweight dev server for the API while developing locally. -• `node public/bundle-api-docs.js` – regenerate bundled OpenAPI docs whenever the YAML files change. -• `git status`, `git diff`, `git log --oneline`, `git add `, `git commit`, `git pull`, `git push` – version control workflow commands. -• `ls` / `dir` / `Get-ChildItem` – inspect directories in PowerShell; `cd` to move between directories. -• `type ` or `Get-Content` – view file contents when tools are not convenient. - +Essential commands for CLQMS development (run from repo root on Windows PowerShell): + +• `composer install` – install PHP dependencies before running CodeIgniter or tests. +• `npm install` – sync `package-lock.json` for tooling such as API docs bundler. +• `./vendor/bin/phpunit` – run entire PHPUnit suite (or target files via `--filter`). +• `php spark test --filter ::` – focused test run when you know the class/method. +• `php spark migrate` / `php spark migrate:rollback` – apply or roll back database migrations. +• `php spark serve` – lightweight dev server for the API while developing locally. +• `node public/bundle-api-docs.js` – regenerate bundled OpenAPI docs whenever the YAML files change. +• `git status`, `git diff`, `git log --oneline`, `git add `, `git commit`, `git pull`, `git push` – version control workflow commands. +• `ls` / `dir` / `Get-ChildItem` – inspect directories in PowerShell; `cd` to move between directories. +• `type ` or `Get-Content` – view file contents when tools are not convenient. + Use these commands routinely after code changes, tests, or migrations. \ No newline at end of file diff --git a/.serena/memories/clqms01-be/task_completion_steps.md b/.serena/memories/clqms01-be/task_completion_steps.md index 24bb5c7..76f7cb4 100644 --- a/.serena/memories/clqms01-be/task_completion_steps.md +++ b/.serena/memories/clqms01-be/task_completion_steps.md @@ -1,10 +1,10 @@ -When a task is completed in CLQMS backend, follow these wrap-up steps: - -1. Run relevant tests (`./vendor/bin/phpunit` or targeted `php spark test --filter ...`). -2. If migrations changed, run `php spark migrate` / `php spark migrate:rollback` locally and ensure schema updates succeed. -3. After editing OpenAPI documentation (YAML files or controller mappings), regenerate `public/api-docs.bundled.yaml` via `node public/bundle-api-docs.js` and check it into Git. -4. Confirm code adheres to PSR-12/CodeIgniter conventions (strict types, response format, transactions, guard clauses) before committing. -5. Review `git status/diff` to ensure only intended files are staged; do not commit `.env` or other secret files. -6. For shared logic changes, double-check lookup JSON cache use and response logging. - +When a task is completed in CLQMS backend, follow these wrap-up steps: + +1. Run relevant tests (`./vendor/bin/phpunit` or targeted `php spark test --filter ...`). +2. If migrations changed, run `php spark migrate` / `php spark migrate:rollback` locally and ensure schema updates succeed. +3. After editing OpenAPI documentation (YAML files or controller mappings), regenerate `public/api-docs.bundled.yaml` via `node public/bundle-api-docs.js` and check it into Git. +4. Confirm code adheres to PSR-12/CodeIgniter conventions (strict types, response format, transactions, guard clauses) before committing. +5. Review `git status/diff` to ensure only intended files are staged; do not commit `.env` or other secret files. +6. For shared logic changes, double-check lookup JSON cache use and response logging. + These steps keep the API consistent, documented, and tested before merging or deploying. \ No newline at end of file diff --git a/.serena/project.yml b/.serena/project.yml index a804b62..ed6d8c4 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,154 +1,154 @@ -# 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 -# haxe 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 - -# advanced configuration option allowing to configure language server-specific options. -# Maps the language key to the options. -# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. -# No documentation on options means no options are available. -ls_specific_settings: {} - -# 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. -# This extends the existing exclusions (e.g. from the global configuration) -# -# 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 based on the project name or path. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly, -# for example by saying that the information retrieved from a memory file is no longer correct -# or no longer relevant for the project. -# * `edit_memory`: Replaces content matching a regular expression in a memory. -# * `execute_shell_command`: Executes a shell command. -# * `find_file`: Finds files in the given relative paths -# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend -# * `find_symbol`: Performs a global (or local) search using the language server backend. -# * `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`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') -# for clients that do not read the initial instructions when the MCP server is connected. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `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`: List available memories. Any memory can be read using the `read_memory` tool. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Read the content of a memory file. This tool should only be used if the information -# is relevant to the current task. You can infer whether the information -# is relevant from the memory file name. -# You should not read the same memory file multiple times in the same conversation. -# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported -# (e.g., renaming "global/foo" to "bar" moves it from global to project scope). -# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities. -# For JB, we use a separate tool. -# * `replace_content`: Replaces content in a file (optionally using regular expressions). -# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend. -# * `safe_delete_symbol`: -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format. -# The memory name should be meaningful. -excluded_tools: [] - -# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). -# This extends the existing inclusions (e.g. from the global configuration). -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: [] - -# list of regex patterns for memories to completely ignore. -# Matching memories will not appear in list_memories or activate_project output -# and cannot be accessed via read_memory or write_memory. -# To access ignored memory files, use the read_file tool on the raw file path. -# Extends the list from the global configuration, merging the two lists. -# Example: ["_archive/.*", "_episodes/.*"] -ignored_memory_patterns: [] +# 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 +# haxe 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 + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +ls_specific_settings: {} + +# 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. +# This extends the existing exclusions (e.g. from the global configuration) +# +# 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 based on the project name or path. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly, +# for example by saying that the information retrieved from a memory file is no longer correct +# or no longer relevant for the project. +# * `edit_memory`: Replaces content matching a regular expression in a memory. +# * `execute_shell_command`: Executes a shell command. +# * `find_file`: Finds files in the given relative paths +# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend +# * `find_symbol`: Performs a global (or local) search using the language server backend. +# * `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`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') +# for clients that do not read the initial instructions when the MCP server is connected. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `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`: List available memories. Any memory can be read using the `read_memory` tool. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Read the content of a memory file. This tool should only be used if the information +# is relevant to the current task. You can infer whether the information +# is relevant from the memory file name. +# You should not read the same memory file multiple times in the same conversation. +# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported +# (e.g., renaming "global/foo" to "bar" moves it from global to project scope). +# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities. +# For JB, we use a separate tool. +# * `replace_content`: Replaces content in a file (optionally using regular expressions). +# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend. +# * `safe_delete_symbol`: +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format. +# The memory name should be meaningful. +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). +# This extends the existing inclusions (e.g. from the global configuration). +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: [] + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] diff --git a/AGENTS.md b/AGENTS.md index a497cf7..9ea1e72 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,153 +1,153 @@ -# AGENTS.md - Code Guidelines for CLQMS - -> **CLQMS (Clinical Laboratory Quality Management System)** – headless REST API backend built on CodeIgniter 4 with a focus on laboratory workflows, JWT authentication, and synchronized OpenAPI documentation. - ---- - -## Repository Snapshot -- `app/` holds controllers, models, filters, and traits wired through PSR-4 `App\` namespace. -- `tests/` relies on CodeIgniter's testing helpers plus Faker for deterministic fixtures. -- Shared response helpers and ValueSet lookups live under `app/Libraries` and `app/Traits` and should be reused before introducing new helpers. -- Environment values, secrets, and database credentials live in `.env` but are never committed; treat the file as a reference for defaults. - ---- - -## Build, Lint & Test -All commands run from the repository root. - -```bash -# Run the entire PHPUnit suite -./vendor/bin/phpunit - -# Target a single test file (fast verification) -./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php - -# Run one test case by method -./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php - -# Generate scaffolding (model, controller, migration) -php spark make:model -php spark make:controller -php spark make:migration - -# Database migrations -php spark migrate -php spark migrate:rollback - -# After OpenAPI edits -node public/bundle-api-docs.js -``` - -Use `php spark test --filter ::` when filtering more than one test file is cumbersome. - ---- - -## Agent Rules Scan -- No `.cursor/rules/*` or `.cursorrules` directory detected; continue without Cursor-specific constraints. -- No `.github/copilot-instructions.md` present; Copilot behaviors revert to general GitHub defaults. - ---- - -## Coding Standards - -### Language & Formatting -- PHP 8.1+ is the baseline; enable `declare(strict_types=1)` at the top of new files when practical. -- Follow PSR-12 for spacing, line length (~120), and brace placement; prefer 4 spaces and avoid tabs. -- Use short arrays `[]`, and wrap multiline arguments/arrays with one-per-line items. -- Favor expression statements that return early (guard clauses) and keep nested logic shallow. -- Keep methods under ~40 lines when possible; extract private helpers for repeated flows. - -### Naming & Types -- Classes, controllers, libraries, and traits: PascalCase (e.g., `PatientImportController`). -- Methods, services, traits: camelCase (`fetchActivePatients`). -- Properties: camelCase for new code; legacy snake_case may persist but avoid new snake_case unless mirroring legacy columns. -- Constants: UPPER_SNAKE_CASE. -- DTOs/array shapes: Use descriptive names (`$patientInput`, `$validatedPayload`). -- Type hints required for method arguments/returns; use union/nullables (e.g., `?string`) instead of doc-only comments. -- Prefer PHPDoc only when type inference fails (complex union or array shapes) but still keep method summaries concise. - -### Imports & Structure -- Namespace declarations at the very top followed by grouped `use` statements. -- Import order: Core framework (`CodeIgniter`), then `App\`, then third-party packages (Firebase, Faker, etc.). Keep each group alphabetical. -- No inline `use` statements inside methods. -- Keep `use` statements de-duplicated; rely on IDE or `phpcbf` to reorder. - -### Controller Structure -- Controllers orchestrate request validation, delegates to services/models, and return `ResponseTrait` responses; avoid direct DB queries here. -- Inject models/services via constructor when they are reused. When instantiating on the fly, reference FQCN (`new \App\Models\...`). -- Map HTTP verbs to semantic methods (`index`, `show`, `create`, `update`, `delete`). Keep action methods under 30 lines by delegating heavy lifting to models or libraries. -- Always respond through `$this->respond()` or `$this->respondCreated()` so JSON structure stays consistent. - -### Response & Error Handling -- All responses follow `{ status, message, data }`. `status` values: `success`, `failed`, or `error`. -- Use `$this->respondCreated()`, `$this->respondNoContent()`, or `$this->respond()` with explicit HTTP codes. -- Wrap JWT/external calls in try/catch. Log unexpected exceptions with `log_message('error', $e->getMessage())` before responding with a sanitized failure. -- For validation failures, return HTTP 400 with detailed message; unauthorized access returns 401. Maintain parity with existing tests. - -### Database & Transactions -- Use Query Builder or Model methods; enable `use App\Models\BaseModel` which handles UTC conversions. -- Always call `helper('utc')` when manipulating timestamps. -- Wrap multi-table changes in `$this->db->transStart()` / `$this->db->transComplete()` and check `transStatus()` to abort if false. -- Run `checkDbError()` (existing helper) after saves when manual queries are necessary. - -### Service Helpers & Libraries -- Encapsulate complex lookups (ValueSet, encryption) inside `app/Libraries` or Traits. -- Reuse `App\Libraries\Lookups` for consistent label/value translations. -- Keep shared logic (e.g., response formatting, JWT decoding) inside Traits and import them via `use`. - -### Testing & Coverage -- Place feature tests under `tests/Feature`, unit tests under `tests/Unit`. -- Test class names should follow `ClassNameTest`; methods follow `test` (e.g., `testCreatePatientValidationFail`). -- Use `FeatureTestTrait` and `CIUnitTestCase` for API tests; prefer `withBodyFormat('json')->post()` flows. -- Assert status codes: 200 for GET/PATCH, 201 for POST, 400 for validation, 401 for auth, 404 for missing resources, 500 for server errors. -- Run targeted tests during development, full suite before merging. - -### Documentation & API Sync -- Whenever a controller or route changes, update `public/paths/.yaml` and matching `public/components/schemas`. Add tags or schema refs in `public/api-docs.yaml`. -- After editing OpenAPI files, regenerate the bundled docs with `node public/bundle-api-docs.js`. Check `public/api-docs.bundled.yaml` into version control. -- Keep the controller-to-YAML mapping table updated to reflect new resources. - -### Routing Conventions -- Keep route definitions grouped inside `$routes->group('api/')` blocks in `app/Config/Routes.php`. -- Prefer nested controllers (e.g., `Patient\PatientController`) for domain partitioning. -- Use RESTful verbs (GET: index/show, POST: create, PATCH: update, DELETE: delete) to keep behavior predictable. -- Document side effects (snapshots, audit logs) directly in the corresponding OpenAPI `paths` file. - -### Environment & Secrets -- Use `.env` as the source of truth for database/jwt settings. Do not commit production credentials. -- Sample values are provided in `.env`; copy to `.env.local` or CI secrets with overrides. -- `JWT_SECRET` must be treated as sensitive and rotated via environment updates only. - -### Workflows & Misc -- Use `php spark migrate`/`migrate:rollback` for schema changes. -- For seeding or test fixtures, prefer factories (Faker) seeded in `tests/Support` when available. -- Document major changes in `issues.md` or dedicated feature docs under `docs/` before merging. - -### Security & Filters -- Apply the `auth` filter to every protected route, and keep `ApiKey` or other custom filters consolidated under `app/Filters`. -- Sanitize user inputs via `filter_var`, `esc()` helpers, or validated entities before they hit the database. -- Always use parameterized queries/Model `save()` methods to prevent SQL injection, especially with legacy PascalCase columns. -- Respond 401 for missing tokens, 403 when permissions fail, and log sanitized details for ops debugging. - -### Legacy Field Naming & ValueSets -- Databases use PascalCase columns such as `PatientID`, `NameFirst`, `CreatedAt`. Keep migration checks aware of these names. -- ValueSet lookups centralize label translation: `Lookups::get('gender')`, `Lookups::getLabel('gender', '1')`, `Lookups::transformLabels($payload, ['Sex' => 'gender'])`. -- Prefer `App\Libraries\Lookups` or `app/Traits/ValueSetTrait` to avoid ad-hoc mappings. - -### Nested Data Handling -- For entities that carry related collections (`PatIdt`, `PatCom`, `PatAtt`), extract nested arrays before filtering and validating. -- Use transactions whenever multi-table inserts/updates occur so orphan rows are avoided. -- Guard against empty/null arrays by normalizing to `[]` before iterating. - -### Observability & Logging -- Use `log_message('info', ...)` for happy-path checkpoints and `'error'` for catch-all failures. -- Avoid leaking sensitive values (tokens, secrets) in logs; log IDs or hash digests instead. -- Keep `writable/logs` clean by rotating or pruning stale log files with automation outside the repo. - ---- - -## Final Notes for Agents -- This repo has no UI layer; focus exclusively on REST interactions. -- Always pull `public/api-docs.bundled.yaml` in after running `node public/bundle-api-docs.js` so downstream services see the latest contract. -- When in doubt, align with existing controller traits and response helpers to avoid duplicating logic. -- Rely on Serena tools for guided edits, searches, and context summaries (use the available symbolic and search tools before running shell commands). +# AGENTS.md - Code Guidelines for CLQMS + +> **CLQMS (Clinical Laboratory Quality Management System)** – headless REST API backend built on CodeIgniter 4 with a focus on laboratory workflows, JWT authentication, and synchronized OpenAPI documentation. + +--- + +## Repository Snapshot +- `app/` holds controllers, models, filters, and traits wired through PSR-4 `App\` namespace. +- `tests/` relies on CodeIgniter's testing helpers plus Faker for deterministic fixtures. +- Shared response helpers and ValueSet lookups live under `app/Libraries` and `app/Traits` and should be reused before introducing new helpers. +- Environment values, secrets, and database credentials live in `.env` but are never committed; treat the file as a reference for defaults. + +--- + +## Build, Lint & Test +All commands run from the repository root. + +```bash +# Run the entire PHPUnit suite +./vendor/bin/phpunit + +# Target a single test file (fast verification) +./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php + +# Run one test case by method +./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php + +# Generate scaffolding (model, controller, migration) +php spark make:model +php spark make:controller +php spark make:migration + +# Database migrations +php spark migrate +php spark migrate:rollback + +# After OpenAPI edits +node public/bundle-api-docs.js +``` + +Use `php spark test --filter ::` when filtering more than one test file is cumbersome. + +--- + +## Agent Rules Scan +- No `.cursor/rules/*` or `.cursorrules` directory detected; continue without Cursor-specific constraints. +- No `.github/copilot-instructions.md` present; Copilot behaviors revert to general GitHub defaults. + +--- + +## Coding Standards + +### Language & Formatting +- PHP 8.1+ is the baseline; enable `declare(strict_types=1)` at the top of new files when practical. +- Follow PSR-12 for spacing, line length (~120), and brace placement; prefer 4 spaces and avoid tabs. +- Use short arrays `[]`, and wrap multiline arguments/arrays with one-per-line items. +- Favor expression statements that return early (guard clauses) and keep nested logic shallow. +- Keep methods under ~40 lines when possible; extract private helpers for repeated flows. + +### Naming & Types +- Classes, controllers, libraries, and traits: PascalCase (e.g., `PatientImportController`). +- Methods, services, traits: camelCase (`fetchActivePatients`). +- Properties: camelCase for new code; legacy snake_case may persist but avoid new snake_case unless mirroring legacy columns. +- Constants: UPPER_SNAKE_CASE. +- DTOs/array shapes: Use descriptive names (`$patientInput`, `$validatedPayload`). +- Type hints required for method arguments/returns; use union/nullables (e.g., `?string`) instead of doc-only comments. +- Prefer PHPDoc only when type inference fails (complex union or array shapes) but still keep method summaries concise. + +### Imports & Structure +- Namespace declarations at the very top followed by grouped `use` statements. +- Import order: Core framework (`CodeIgniter`), then `App\`, then third-party packages (Firebase, Faker, etc.). Keep each group alphabetical. +- No inline `use` statements inside methods. +- Keep `use` statements de-duplicated; rely on IDE or `phpcbf` to reorder. + +### Controller Structure +- Controllers orchestrate request validation, delegates to services/models, and return `ResponseTrait` responses; avoid direct DB queries here. +- Inject models/services via constructor when they are reused. When instantiating on the fly, reference FQCN (`new \App\Models\...`). +- Map HTTP verbs to semantic methods (`index`, `show`, `create`, `update`, `delete`). Keep action methods under 30 lines by delegating heavy lifting to models or libraries. +- Always respond through `$this->respond()` or `$this->respondCreated()` so JSON structure stays consistent. + +### Response & Error Handling +- All responses follow `{ status, message, data }`. `status` values: `success`, `failed`, or `error`. +- Use `$this->respondCreated()`, `$this->respondNoContent()`, or `$this->respond()` with explicit HTTP codes. +- Wrap JWT/external calls in try/catch. Log unexpected exceptions with `log_message('error', $e->getMessage())` before responding with a sanitized failure. +- For validation failures, return HTTP 400 with detailed message; unauthorized access returns 401. Maintain parity with existing tests. + +### Database & Transactions +- Use Query Builder or Model methods; enable `use App\Models\BaseModel` which handles UTC conversions. +- Always call `helper('utc')` when manipulating timestamps. +- Wrap multi-table changes in `$this->db->transStart()` / `$this->db->transComplete()` and check `transStatus()` to abort if false. +- Run `checkDbError()` (existing helper) after saves when manual queries are necessary. + +### Service Helpers & Libraries +- Encapsulate complex lookups (ValueSet, encryption) inside `app/Libraries` or Traits. +- Reuse `App\Libraries\Lookups` for consistent label/value translations. +- Keep shared logic (e.g., response formatting, JWT decoding) inside Traits and import them via `use`. + +### Testing & Coverage +- Place feature tests under `tests/Feature`, unit tests under `tests/Unit`. +- Test class names should follow `ClassNameTest`; methods follow `test` (e.g., `testCreatePatientValidationFail`). +- Use `FeatureTestTrait` and `CIUnitTestCase` for API tests; prefer `withBodyFormat('json')->post()` flows. +- Assert status codes: 200 for GET/PATCH, 201 for POST, 400 for validation, 401 for auth, 404 for missing resources, 500 for server errors. +- Run targeted tests during development, full suite before merging. + +### Documentation & API Sync +- Whenever a controller or route changes, update `public/paths/.yaml` and matching `public/components/schemas`. Add tags or schema refs in `public/api-docs.yaml`. +- After editing OpenAPI files, regenerate the bundled docs with `node public/bundle-api-docs.js`. Check `public/api-docs.bundled.yaml` into version control. +- Keep the controller-to-YAML mapping table updated to reflect new resources. + +### Routing Conventions +- Keep route definitions grouped inside `$routes->group('api/')` blocks in `app/Config/Routes.php`. +- Prefer nested controllers (e.g., `Patient\PatientController`) for domain partitioning. +- Use RESTful verbs (GET: index/show, POST: create, PATCH: update, DELETE: delete) to keep behavior predictable. +- Document side effects (snapshots, audit logs) directly in the corresponding OpenAPI `paths` file. + +### Environment & Secrets +- Use `.env` as the source of truth for database/jwt settings. Do not commit production credentials. +- Sample values are provided in `.env`; copy to `.env.local` or CI secrets with overrides. +- `JWT_SECRET` must be treated as sensitive and rotated via environment updates only. + +### Workflows & Misc +- Use `php spark migrate`/`migrate:rollback` for schema changes. +- For seeding or test fixtures, prefer factories (Faker) seeded in `tests/Support` when available. +- Document major changes in `issues.md` or dedicated feature docs under `docs/` before merging. + +### Security & Filters +- Apply the `auth` filter to every protected route, and keep `ApiKey` or other custom filters consolidated under `app/Filters`. +- Sanitize user inputs via `filter_var`, `esc()` helpers, or validated entities before they hit the database. +- Always use parameterized queries/Model `save()` methods to prevent SQL injection, especially with legacy PascalCase columns. +- Respond 401 for missing tokens, 403 when permissions fail, and log sanitized details for ops debugging. + +### Legacy Field Naming & ValueSets +- Databases use PascalCase columns such as `PatientID`, `NameFirst`, `CreatedAt`. Keep migration checks aware of these names. +- ValueSet lookups centralize label translation: `Lookups::get('gender')`, `Lookups::getLabel('gender', '1')`, `Lookups::transformLabels($payload, ['Sex' => 'gender'])`. +- Prefer `App\Libraries\Lookups` or `app/Traits/ValueSetTrait` to avoid ad-hoc mappings. + +### Nested Data Handling +- For entities that carry related collections (`PatIdt`, `PatCom`, `PatAtt`), extract nested arrays before filtering and validating. +- Use transactions whenever multi-table inserts/updates occur so orphan rows are avoided. +- Guard against empty/null arrays by normalizing to `[]` before iterating. + +### Observability & Logging +- Use `log_message('info', ...)` for happy-path checkpoints and `'error'` for catch-all failures. +- Avoid leaking sensitive values (tokens, secrets) in logs; log IDs or hash digests instead. +- Keep `writable/logs` clean by rotating or pruning stale log files with automation outside the repo. + +--- + +## Final Notes for Agents +- This repo has no UI layer; focus exclusively on REST interactions. +- Always pull `public/api-docs.bundled.yaml` in after running `node public/bundle-api-docs.js` so downstream services see the latest contract. +- When in doubt, align with existing controller traits and response helpers to avoid duplicating logic. +- Rely on Serena tools for guided edits, searches, and context summaries (use the available symbolic and search tools before running shell commands). diff --git a/app/Controllers/Audit/AuditLogController.php b/app/Controllers/Audit/AuditLogController.php index f74984a..8a2fe37 100755 --- a/app/Controllers/Audit/AuditLogController.php +++ b/app/Controllers/Audit/AuditLogController.php @@ -1,60 +1,60 @@ -auditLogService = new AuditLogService(); - } - - public function index(): ResponseInterface - { - $filters = [ - 'table' => $this->request->getGet('table'), - 'rec_id' => $this->request->getGet('rec_id') ?? $this->request->getGet('recId'), - 'event_id' => $this->request->getGet('event_id') ?? $this->request->getGet('eventId'), - 'activity_id' => $this->request->getGet('activity_id') ?? $this->request->getGet('activityId'), - 'from' => $this->request->getGet('from'), - 'to' => $this->request->getGet('to'), - 'search' => $this->request->getGet('search'), - 'page' => $this->request->getGet('page'), - 'perPage' => $this->request->getGet('perPage') ?? $this->request->getGet('per_page'), - ]; - - try { - $payload = $this->auditLogService->fetchLogs($filters); - - return $this->respond([ - 'status' => 'success', - 'message' => 'Audit logs retrieved successfully', - 'data' => $payload, - ], 200); - - } catch (InvalidArgumentException $e) { - return $this->respond([ - 'status' => 'failed', - 'message' => $e->getMessage(), - 'data' => null, - ], 400); - } catch (\Throwable $e) { - log_message('error', 'AuditLogController::index error: ' . $e->getMessage()); - return $this->respond([ - 'status' => 'failed', - 'message' => 'Unable to retrieve audit logs', - 'data' => null, - ], 500); - } - } -} +auditLogService = new AuditLogService(); + } + + public function index(): ResponseInterface + { + $filters = [ + 'table' => $this->request->getGet('table'), + 'rec_id' => $this->request->getGet('rec_id') ?? $this->request->getGet('recId'), + 'event_id' => $this->request->getGet('event_id') ?? $this->request->getGet('eventId'), + 'activity_id' => $this->request->getGet('activity_id') ?? $this->request->getGet('activityId'), + 'from' => $this->request->getGet('from'), + 'to' => $this->request->getGet('to'), + 'search' => $this->request->getGet('search'), + 'page' => $this->request->getGet('page'), + 'perPage' => $this->request->getGet('perPage') ?? $this->request->getGet('per_page'), + ]; + + try { + $payload = $this->auditLogService->fetchLogs($filters); + + return $this->respond([ + 'status' => 'success', + 'message' => 'Audit logs retrieved successfully', + 'data' => $payload, + ], 200); + + } catch (InvalidArgumentException $e) { + return $this->respond([ + 'status' => 'failed', + 'message' => $e->getMessage(), + 'data' => null, + ], 400); + } catch (\Throwable $e) { + log_message('error', 'AuditLogController::index error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Unable to retrieve audit logs', + 'data' => null, + ], 500); + } + } +} diff --git a/app/Controllers/Contact/ContactController.php b/app/Controllers/Contact/ContactController.php index be70628..89d6db3 100755 --- a/app/Controllers/Contact/ContactController.php +++ b/app/Controllers/Contact/ContactController.php @@ -1,73 +1,73 @@ -db = \Config\Database::connect(); $this->model = new ContactModel(); $this->rules = [ 'NameFirst' => 'required' ]; $this->patchRules = [ 'NameFirst' => 'permit_empty' ]; } - - public function index() { - $ContactName = $this->request->getVar('ContactName'); - $Specialty = $this->request->getVar('Specialty'); - $rows = $this->model->getContacts($ContactName, $Specialty); - - if (empty($rows)) { - return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); - } - - $rows = ValueSet::transformLabels($rows, [ - 'Specialty' => 'specialty', - 'Occupation' => 'occupation', - ]); - - return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200); - } - - public function show($ContactID = null) { - $model = new ContactModel(); - $row = $model->getContactWithDetail($ContactID); - - if (empty($row)) { - return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); - } - - $row = ValueSet::transformLabels([$row], [ - 'Specialty' => 'specialty', - 'Occupation' => 'occupation', - ])[0]; - - return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200); - } - - public function delete() { - try { - $input = $this->request->getJSON(true); - $ContactID = $input["ContactID"]; - if (!$ContactID) { return $this->failValidationErrors('ContactID is required.'); } - $this->model->delete($ContactID); - return $this->respondDeleted([ 'status' => 'success', 'message' => "Contact with {$ContactID} deleted successfully."]); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - + + public function index() { + $ContactName = $this->request->getVar('ContactName'); + $Specialty = $this->request->getVar('Specialty'); + $rows = $this->model->getContacts($ContactName, $Specialty); + + if (empty($rows)) { + return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); + } + + $rows = ValueSet::transformLabels($rows, [ + 'Specialty' => 'specialty', + 'Occupation' => 'occupation', + ]); + + return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200); + } + + public function show($ContactID = null) { + $model = new ContactModel(); + $row = $model->getContactWithDetail($ContactID); + + if (empty($row)) { + return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); + } + + $row = ValueSet::transformLabels([$row], [ + 'Specialty' => 'specialty', + 'Occupation' => 'occupation', + ])[0]; + + return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200); + } + + public function delete() { + try { + $input = $this->request->getJSON(true); + $ContactID = $input["ContactID"]; + if (!$ContactID) { return $this->failValidationErrors('ContactID is required.'); } + $this->model->delete($ContactID); + return $this->respondDeleted([ 'status' => 'success', 'message' => "Contact with {$ContactID} deleted successfully."]); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + public function create() { $input = $this->request->getJSON(true); if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } @@ -87,7 +87,7 @@ class ContactController extends BaseController { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } - + public function update($ContactID = null) { $input = $this->requirePatchPayload($this->request->getJSON(true)); if ($input === null) { @@ -115,9 +115,17 @@ class ContactController extends BaseController { $input['ContactID'] = $id; try { - $this->model->saveContact($input); - $id = $input['ContactID']; - return $this->respond([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 200); + $result = $this->model->saveContact($input); + + if (($result['status'] ?? 'error') !== 'success') { + return $this->respond([ + 'status' => 'failed', + 'message' => $result['message'] ?? 'Failed to update contact', + 'data' => [] + ], 400); + } + + return $this->respond([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $result ], 200); } catch (\Throwable $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } diff --git a/app/Controllers/Organization/CodingSysController.php b/app/Controllers/Organization/CodingSysController.php index 74b2e44..ffaa913 100755 --- a/app/Controllers/Organization/CodingSysController.php +++ b/app/Controllers/Organization/CodingSysController.php @@ -1,112 +1,112 @@ -db = \Config\Database::connect(); - $this->model = new CodingSysModel(); - } - - public function index() { - $filter = [ - 'CodingSysAbb' => $this->request->getVar('CodingSysAbb'), - 'FullText' => $this->request->getVar('FullText'), - ]; - - $builder = $this->model; - - if (!empty($filter['CodingSysAbb'])) { - $builder->like('CodingSysAbb', $filter['CodingSysAbb'], 'both'); - } - if (!empty($filter['FullText'])) { - $builder->like('FullText', $filter['FullText'], 'both'); - } - - $rows = $builder->findAll(); - - if (empty($rows)) { - return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => []], 200); - } - - return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $rows], 200); - } - - public function show($CodingSysID = null) { - $row = $this->model->where('CodingSysID', $CodingSysID)->first(); - - if (empty($row)) { - return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => null], 200); - } - - return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $row], 200); - } - - public function delete() { - try { - $input = $this->request->getJSON(true); - $id = $input['CodingSysID'] ?? null; - - if (!$id) { - return $this->failValidationErrors('CodingSysID is required.'); - } - - $this->model->delete($id); - return $this->respondDeleted(['status' => 'success', 'message' => "{$id} deleted successfully."]); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function create() { - $input = $this->request->getJSON(true); - - try { - $id = $this->model->insert($input, true); - return $this->respondCreated(['status' => 'success', 'message' => 'data created successfully', 'data' => $id], 201); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function update($CodingSysID = null) { - $input = $this->requirePatchPayload($this->request->getJSON(true)); - if ($input === null) { - return; - } - - $id = $this->requirePatchId($CodingSysID, 'CodingSysID'); - if ($id === null) { - return; - } - - $existing = $this->model->find($id); - if (!$existing) { - return $this->respond(['status' => 'failed', 'message' => 'CodingSys not found', 'data' => []], 404); - } - - if (isset($input['CodingSysID']) && (string) $input['CodingSysID'] !== (string) $id) { - return $this->failValidationErrors('CodingSysID in URL does not match body.'); - } - - $input['CodingSysID'] = $id; - - try { - $this->model->update($id, $input); - return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } -} +db = \Config\Database::connect(); + $this->model = new CodingSysModel(); + } + + public function index() { + $filter = [ + 'CodingSysAbb' => $this->request->getVar('CodingSysAbb'), + 'FullText' => $this->request->getVar('FullText'), + ]; + + $builder = $this->model; + + if (!empty($filter['CodingSysAbb'])) { + $builder->like('CodingSysAbb', $filter['CodingSysAbb'], 'both'); + } + if (!empty($filter['FullText'])) { + $builder->like('FullText', $filter['FullText'], 'both'); + } + + $rows = $builder->findAll(); + + if (empty($rows)) { + return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => []], 200); + } + + return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $rows], 200); + } + + public function show($CodingSysID = null) { + $row = $this->model->where('CodingSysID', $CodingSysID)->first(); + + if (empty($row)) { + return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => null], 200); + } + + return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $row], 200); + } + + public function delete() { + try { + $input = $this->request->getJSON(true); + $id = $input['CodingSysID'] ?? null; + + if (!$id) { + return $this->failValidationErrors('CodingSysID is required.'); + } + + $this->model->delete($id); + return $this->respondDeleted(['status' => 'success', 'message' => "{$id} deleted successfully."]); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function create() { + $input = $this->request->getJSON(true); + + try { + $id = $this->model->insert($input, true); + return $this->respondCreated(['status' => 'success', 'message' => 'data created successfully', 'data' => $id], 201); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function update($CodingSysID = null) { + $input = $this->requirePatchPayload($this->request->getJSON(true)); + if ($input === null) { + return; + } + + $id = $this->requirePatchId($CodingSysID, 'CodingSysID'); + if ($id === null) { + return; + } + + $existing = $this->model->find($id); + if (!$existing) { + return $this->respond(['status' => 'failed', 'message' => 'CodingSys not found', 'data' => []], 404); + } + + if (isset($input['CodingSysID']) && (string) $input['CodingSysID'] !== (string) $id) { + return $this->failValidationErrors('CodingSysID in URL does not match body.'); + } + + $input['CodingSysID'] = $id; + + try { + $this->model->update($id, $input); + return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } +} diff --git a/app/Controllers/Organization/HostAppController.php b/app/Controllers/Organization/HostAppController.php index 4a077a7..64973d4 100755 --- a/app/Controllers/Organization/HostAppController.php +++ b/app/Controllers/Organization/HostAppController.php @@ -1,124 +1,124 @@ -db = \Config\Database::connect(); - $this->model = new HostAppModel(); - } - - public function index() { - $filter = [ - 'HostAppID' => $this->request->getVar('HostAppID'), - 'HostAppName' => $this->request->getVar('HostAppName'), - ]; - - $builder = $this->model->select('hostapp.*, site.SiteName') - ->join('site', 'site.SiteID = hostapp.SiteID', 'left'); - - if (!empty($filter['HostAppID'])) { - if (!ctype_digit((string) $filter['HostAppID'])) { - return $this->failValidationErrors('HostAppID filter must be a valid integer.'); - } - $builder->where('hostapp.HostAppID', (int) $filter['HostAppID']); - } - if (!empty($filter['HostAppName'])) { - $builder->like('hostapp.HostAppName', $filter['HostAppName'], 'both'); - } - - $rows = $builder->findAll(); - - if (empty($rows)) { - return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => []], 200); - } - - return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $rows], 200); - } - - public function show($HostAppID = null) { - $id = $this->requirePatchId($HostAppID, 'HostAppID'); - if ($id === null) { - return; - } - - $row = $this->model->select('hostapp.*, site.SiteName') - ->join('site', 'site.SiteID = hostapp.SiteID', 'left') - ->where('hostapp.HostAppID', $id) - ->first(); - - if (empty($row)) { - return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => null], 200); - } - - return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $row], 200); - } - - public function delete() { - try { - $input = $this->request->getJSON(true); - $id = $this->requirePatchId($input['HostAppID'] ?? null, 'HostAppID'); - if ($id === null) { - return; - } - - $this->model->delete($id); - return $this->respondDeleted(['status' => 'success', 'message' => "{$id} deleted successfully."]); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function create() { - $input = $this->request->getJSON(true); - - try { - unset($input['HostAppID']); - $id = $this->model->insert($input, true); - return $this->respondCreated(['status' => 'success', 'message' => 'data created successfully', 'data' => $id], 201); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function update($HostAppID = null) { - $input = $this->requirePatchPayload($this->request->getJSON(true)); - if ($input === null) { - return; - } - - $id = $this->requirePatchId($HostAppID, 'HostAppID'); - if ($id === null) { - return; - } - - $existing = $this->model->find($id); - if (!$existing) { - return $this->respond(['status' => 'failed', 'message' => 'HostApp not found', 'data' => []], 404); - } - - if (isset($input['HostAppID'])) { - if ((string) $input['HostAppID'] !== (string) $id) { - return $this->failValidationErrors('HostAppID in URL does not match body.'); - } - unset($input['HostAppID']); - } - try { - $this->model->update($id, $input); - return $this->respond(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 200); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } -} +db = \Config\Database::connect(); + $this->model = new HostAppModel(); + } + + public function index() { + $filter = [ + 'HostAppID' => $this->request->getVar('HostAppID'), + 'HostAppName' => $this->request->getVar('HostAppName'), + ]; + + $builder = $this->model->select('hostapp.*, site.SiteName') + ->join('site', 'site.SiteID = hostapp.SiteID', 'left'); + + if (!empty($filter['HostAppID'])) { + if (!ctype_digit((string) $filter['HostAppID'])) { + return $this->failValidationErrors('HostAppID filter must be a valid integer.'); + } + $builder->where('hostapp.HostAppID', (int) $filter['HostAppID']); + } + if (!empty($filter['HostAppName'])) { + $builder->like('hostapp.HostAppName', $filter['HostAppName'], 'both'); + } + + $rows = $builder->findAll(); + + if (empty($rows)) { + return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => []], 200); + } + + return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $rows], 200); + } + + public function show($HostAppID = null) { + $id = $this->requirePatchId($HostAppID, 'HostAppID'); + if ($id === null) { + return; + } + + $row = $this->model->select('hostapp.*, site.SiteName') + ->join('site', 'site.SiteID = hostapp.SiteID', 'left') + ->where('hostapp.HostAppID', $id) + ->first(); + + if (empty($row)) { + return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => null], 200); + } + + return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $row], 200); + } + + public function delete() { + try { + $input = $this->request->getJSON(true); + $id = $this->requirePatchId($input['HostAppID'] ?? null, 'HostAppID'); + if ($id === null) { + return; + } + + $this->model->delete($id); + return $this->respondDeleted(['status' => 'success', 'message' => "{$id} deleted successfully."]); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function create() { + $input = $this->request->getJSON(true); + + try { + unset($input['HostAppID']); + $id = $this->model->insert($input, true); + return $this->respondCreated(['status' => 'success', 'message' => 'data created successfully', 'data' => $id], 201); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function update($HostAppID = null) { + $input = $this->requirePatchPayload($this->request->getJSON(true)); + if ($input === null) { + return; + } + + $id = $this->requirePatchId($HostAppID, 'HostAppID'); + if ($id === null) { + return; + } + + $existing = $this->model->find($id); + if (!$existing) { + return $this->respond(['status' => 'failed', 'message' => 'HostApp not found', 'data' => []], 404); + } + + if (isset($input['HostAppID'])) { + if ((string) $input['HostAppID'] !== (string) $id) { + return $this->failValidationErrors('HostAppID in URL does not match body.'); + } + unset($input['HostAppID']); + } + try { + $this->model->update($id, $input); + return $this->respond(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 200); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } +} diff --git a/app/Controllers/Organization/HostComParaController.php b/app/Controllers/Organization/HostComParaController.php index e453070..3c52853 100755 --- a/app/Controllers/Organization/HostComParaController.php +++ b/app/Controllers/Organization/HostComParaController.php @@ -1,135 +1,135 @@ -db = \Config\Database::connect(); - $this->model = new HostComParaModel(); - } - - public function index() { - $filter = [ - 'HostAppID' => $this->request->getVar('HostAppID'), - 'HostIP' => $this->request->getVar('HostIP'), - ]; - - $builder = $this->model->select('hostcompara.*, hostapp.HostAppName') - ->join('hostapp', 'hostapp.HostAppID = hostcompara.HostAppID', 'left'); - - if (!empty($filter['HostAppID'])) { - if (!ctype_digit((string) $filter['HostAppID'])) { - return $this->failValidationErrors('HostAppID filter must be a valid integer.'); - } - $builder->where('hostcompara.HostAppID', (int) $filter['HostAppID']); - } - if (!empty($filter['HostIP'])) { - $builder->like('hostcompara.HostIP', $filter['HostIP'], 'both'); - } - - $rows = $builder->findAll(); - - if (empty($rows)) { - return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => []], 200); - } - - return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $rows], 200); - } - - public function show($HostAppID = null) { - $id = $this->requirePatchId($HostAppID, 'HostAppID'); - if ($id === null) { - return; - } - - $row = $this->model->select('hostcompara.*, hostapp.HostAppName') - ->join('hostapp', 'hostapp.HostAppID = hostcompara.HostAppID', 'left') - ->where('hostcompara.HostAppID', $id) - ->first(); - - if (empty($row)) { - return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => null], 200); - } - - return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $row], 200); - } - - public function delete() { - try { - $input = $this->request->getJSON(true); - $id = $this->requirePatchId($input['HostAppID'] ?? null, 'HostAppID'); - if ($id === null) { - return; - } - - $this->model->delete($id); - return $this->respondDeleted(['status' => 'success', 'message' => "{$id} deleted successfully."]); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function create() { - $input = $this->request->getJSON(true); - - try { - $hostAppId = $input['HostAppID'] ?? null; - if ($hostAppId === null) { - return $this->failValidationErrors('HostAppID is required.'); - } - - if (!ctype_digit((string) $hostAppId)) { - return $this->failValidationErrors('HostAppID must be a valid integer.'); - } - - $input['HostAppID'] = (int) $hostAppId; - - $id = $this->model->insert($input, true); - return $this->respondCreated(['status' => 'success', 'message' => 'data created successfully', 'data' => $id], 201); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function update($HostAppID = null) { - $input = $this->requirePatchPayload($this->request->getJSON(true)); - if ($input === null) { - return; - } - - $id = $this->requirePatchId($HostAppID, 'HostAppID'); - if ($id === null) { - return; - } - - $existing = $this->model->find($id); - if (!$existing) { - return $this->respond(['status' => 'failed', 'message' => 'HostComPara not found', 'data' => []], 404); - } - - if (isset($input['HostAppID'])) { - if ((string) $input['HostAppID'] !== (string) $id) { - return $this->failValidationErrors('HostAppID in URL does not match body.'); - } - unset($input['HostAppID']); - } - - try { - $this->model->update($id, $input); - return $this->respond(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 200); - } catch (\Throwable $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } -} +db = \Config\Database::connect(); + $this->model = new HostComParaModel(); + } + + public function index() { + $filter = [ + 'HostAppID' => $this->request->getVar('HostAppID'), + 'HostIP' => $this->request->getVar('HostIP'), + ]; + + $builder = $this->model->select('hostcompara.*, hostapp.HostAppName') + ->join('hostapp', 'hostapp.HostAppID = hostcompara.HostAppID', 'left'); + + if (!empty($filter['HostAppID'])) { + if (!ctype_digit((string) $filter['HostAppID'])) { + return $this->failValidationErrors('HostAppID filter must be a valid integer.'); + } + $builder->where('hostcompara.HostAppID', (int) $filter['HostAppID']); + } + if (!empty($filter['HostIP'])) { + $builder->like('hostcompara.HostIP', $filter['HostIP'], 'both'); + } + + $rows = $builder->findAll(); + + if (empty($rows)) { + return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => []], 200); + } + + return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $rows], 200); + } + + public function show($HostAppID = null) { + $id = $this->requirePatchId($HostAppID, 'HostAppID'); + if ($id === null) { + return; + } + + $row = $this->model->select('hostcompara.*, hostapp.HostAppName') + ->join('hostapp', 'hostapp.HostAppID = hostcompara.HostAppID', 'left') + ->where('hostcompara.HostAppID', $id) + ->first(); + + if (empty($row)) { + return $this->respond(['status' => 'success', 'message' => 'no Data.', 'data' => null], 200); + } + + return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $row], 200); + } + + public function delete() { + try { + $input = $this->request->getJSON(true); + $id = $this->requirePatchId($input['HostAppID'] ?? null, 'HostAppID'); + if ($id === null) { + return; + } + + $this->model->delete($id); + return $this->respondDeleted(['status' => 'success', 'message' => "{$id} deleted successfully."]); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function create() { + $input = $this->request->getJSON(true); + + try { + $hostAppId = $input['HostAppID'] ?? null; + if ($hostAppId === null) { + return $this->failValidationErrors('HostAppID is required.'); + } + + if (!ctype_digit((string) $hostAppId)) { + return $this->failValidationErrors('HostAppID must be a valid integer.'); + } + + $input['HostAppID'] = (int) $hostAppId; + + $id = $this->model->insert($input, true); + return $this->respondCreated(['status' => 'success', 'message' => 'data created successfully', 'data' => $id], 201); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function update($HostAppID = null) { + $input = $this->requirePatchPayload($this->request->getJSON(true)); + if ($input === null) { + return; + } + + $id = $this->requirePatchId($HostAppID, 'HostAppID'); + if ($id === null) { + return; + } + + $existing = $this->model->find($id); + if (!$existing) { + return $this->respond(['status' => 'failed', 'message' => 'HostComPara not found', 'data' => []], 404); + } + + if (isset($input['HostAppID'])) { + if ((string) $input['HostAppID'] !== (string) $id) { + return $this->failValidationErrors('HostAppID in URL does not match body.'); + } + unset($input['HostAppID']); + } + + try { + $this->model->update($id, $input); + return $this->respond(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 200); + } catch (\Throwable $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } +} diff --git a/app/Controllers/User/UserController.php b/app/Controllers/User/UserController.php index 0b7c357..defb6a1 100755 --- a/app/Controllers/User/UserController.php +++ b/app/Controllers/User/UserController.php @@ -1,306 +1,306 @@ -db = \Config\Database::connect(); - $this->model = new UserModel(); - } - - /** - * List users with pagination and search - * GET /api/user?page=1&per_page=20&search=term - */ - public function index() - { - try { - $page = (int)($this->request->getGet('page') ?? 1); - $perPage = (int)($this->request->getGet('per_page') ?? 20); - $search = $this->request->getGet('search'); - - // Build query - $builder = $this->model->where('DelDate', null); - - // Apply search if provided - if ($search) { - $builder->groupStart() - ->like('Username', $search) - ->orLike('Email', $search) - ->orLike('Name', $search) - ->groupEnd(); - } - - // Get total count for pagination - $total = $builder->countAllResults(false); - - // Get paginated results - $users = $builder - ->orderBy('UserID', 'DESC') - ->limit($perPage, ($page - 1) * $perPage) - ->findAll(); - - return $this->respond([ - 'status' => 'success', - 'message' => 'Users retrieved successfully', - 'data' => [ - 'users' => $users, - 'pagination' => [ - 'current_page' => $page, - 'per_page' => $perPage, - 'total' => $total, - 'total_pages' => ceil($total / $perPage) - ] - ] - ], 200); - - } catch (\Exception $e) { - log_message('error', 'UserController::index error: ' . $e->getMessage()); - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to retrieve users', - 'data' => null - ], 500); - } - } - - /** - * Get single user by ID - * GET /api/user/(:num) - */ - public function show($id) - { - try { - $user = $this->model->where('UserID', $id) - ->where('DelDate', null) - ->first(); - - if (empty($user)) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'User not found', - 'data' => null - ], 404); - } - - return $this->respond([ - 'status' => 'success', - 'message' => 'User retrieved successfully', - 'data' => $user - ], 200); - - } catch (\Exception $e) { - log_message('error', 'UserController::show error: ' . $e->getMessage()); - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to retrieve user', - 'data' => null - ], 500); - } - } - - /** - * Create new user - * POST /api/user - */ - public function create() - { - try { - $data = $this->request->getJSON(true); - - // Validation rules - $rules = [ - 'Username' => 'required|min_length[3]|max_length[50]|is_unique[users.Username]', - 'Email' => 'required|valid_email|is_unique[users.Email]', - ]; - - if (!$this->validateData($data, $rules)) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'Validation failed', - 'data' => $this->validator->getErrors() - ], 400); - } - - // Set default values - $data['IsActive'] = $data['IsActive'] ?? true; - $data['CreatedAt'] = date('Y-m-d H:i:s'); - - $userId = $this->model->insert($data); - - if (!$userId) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to create user', - 'data' => null - ], 500); - } - - return $this->respond([ - 'status' => 'success', - 'message' => 'User created successfully', - 'data' => [ - 'UserID' => $userId, - 'Username' => $data['Username'], - 'Email' => $data['Email'] - ] - ], 201); - - } catch (\Exception $e) { - log_message('error', 'UserController::create error: ' . $e->getMessage()); - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to create user', - 'data' => null - ], 500); - } - } - - /** - * Update existing user - * PATCH /api/user/(:num) - */ - public function update($id) - { - try { - $data = $this->requirePatchPayload($this->request->getJSON(true)); - if ($data === null) { - return; - } - - if (empty($id) || !ctype_digit((string) $id)) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'UserID is required', - 'data' => null - ], 400); - } - - if (isset($data['UserID']) && (string) $data['UserID'] !== (string) $id) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'UserID in URL does not match body', - 'data' => null - ], 400); - } - - $userId = (int) $id; - - // Check if user exists - $user = $this->model->where('UserID', $userId) - ->where('DelDate', null) - ->first(); - - if (empty($user)) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'User not found', - 'data' => null - ], 404); - } - - // Remove UserID from data array - unset($data['UserID']); - - // Don't allow updating these fields - unset($data['CreatedAt']); - unset($data['Username']); // Username should not change - - $data['UpdatedAt'] = date('Y-m-d H:i:s'); - - $updated = $this->model->update($userId, $data); - - if (!$updated) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to update user', - 'data' => null - ], 500); - } - - return $this->respond([ - 'status' => 'success', - 'message' => 'User updated successfully', - 'data' => [ - 'UserID' => $userId, - 'updated_fields' => array_keys($data) - ] - ], 200); - - } catch (\Exception $e) { - log_message('error', 'UserController::update error: ' . $e->getMessage()); - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to update user', - 'data' => null - ], 500); - } - } - - /** - * Delete user (soft delete) - * DELETE /api/user/(:num) - */ - public function delete($id) - { - try { - // Check if user exists - $user = $this->model->where('UserID', $id) - ->where('DelDate', null) - ->first(); - - if (empty($user)) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'User not found', - 'data' => null - ], 404); - } - - // Soft delete by setting DelDate - $deleted = $this->model->update($id, [ - 'DelDate' => date('Y-m-d H:i:s'), - 'IsActive' => false - ]); - - if (!$deleted) { - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to delete user', - 'data' => null - ], 500); - } - - return $this->respond([ - 'status' => 'success', - 'message' => 'User deleted successfully', - 'data' => ['UserID' => $id] - ], 200); - - } catch (\Exception $e) { - log_message('error', 'UserController::delete error: ' . $e->getMessage()); - return $this->respond([ - 'status' => 'failed', - 'message' => 'Failed to delete user', - 'data' => null - ], 500); - } - } -} +db = \Config\Database::connect(); + $this->model = new UserModel(); + } + + /** + * List users with pagination and search + * GET /api/user?page=1&per_page=20&search=term + */ + public function index() + { + try { + $page = (int)($this->request->getGet('page') ?? 1); + $perPage = (int)($this->request->getGet('per_page') ?? 20); + $search = $this->request->getGet('search'); + + // Build query + $builder = $this->model->where('DelDate', null); + + // Apply search if provided + if ($search) { + $builder->groupStart() + ->like('Username', $search) + ->orLike('Email', $search) + ->orLike('Name', $search) + ->groupEnd(); + } + + // Get total count for pagination + $total = $builder->countAllResults(false); + + // Get paginated results + $users = $builder + ->orderBy('UserID', 'DESC') + ->limit($perPage, ($page - 1) * $perPage) + ->findAll(); + + return $this->respond([ + 'status' => 'success', + 'message' => 'Users retrieved successfully', + 'data' => [ + 'users' => $users, + 'pagination' => [ + 'current_page' => $page, + 'per_page' => $perPage, + 'total' => $total, + 'total_pages' => ceil($total / $perPage) + ] + ] + ], 200); + + } catch (\Exception $e) { + log_message('error', 'UserController::index error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to retrieve users', + 'data' => null + ], 500); + } + } + + /** + * Get single user by ID + * GET /api/user/(:num) + */ + public function show($id) + { + try { + $user = $this->model->where('UserID', $id) + ->where('DelDate', null) + ->first(); + + if (empty($user)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'User not found', + 'data' => null + ], 404); + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'User retrieved successfully', + 'data' => $user + ], 200); + + } catch (\Exception $e) { + log_message('error', 'UserController::show error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to retrieve user', + 'data' => null + ], 500); + } + } + + /** + * Create new user + * POST /api/user + */ + public function create() + { + try { + $data = $this->request->getJSON(true); + + // Validation rules + $rules = [ + 'Username' => 'required|min_length[3]|max_length[50]|is_unique[users.Username]', + 'Email' => 'required|valid_email|is_unique[users.Email]', + ]; + + if (!$this->validateData($data, $rules)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Validation failed', + 'data' => $this->validator->getErrors() + ], 400); + } + + // Set default values + $data['IsActive'] = $data['IsActive'] ?? true; + $data['CreatedAt'] = date('Y-m-d H:i:s'); + + $userId = $this->model->insert($data); + + if (!$userId) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to create user', + 'data' => null + ], 500); + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'User created successfully', + 'data' => [ + 'UserID' => $userId, + 'Username' => $data['Username'], + 'Email' => $data['Email'] + ] + ], 201); + + } catch (\Exception $e) { + log_message('error', 'UserController::create error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to create user', + 'data' => null + ], 500); + } + } + + /** + * Update existing user + * PATCH /api/user/(:num) + */ + public function update($id) + { + try { + $data = $this->requirePatchPayload($this->request->getJSON(true)); + if ($data === null) { + return; + } + + if (empty($id) || !ctype_digit((string) $id)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'UserID is required', + 'data' => null + ], 400); + } + + if (isset($data['UserID']) && (string) $data['UserID'] !== (string) $id) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'UserID in URL does not match body', + 'data' => null + ], 400); + } + + $userId = (int) $id; + + // Check if user exists + $user = $this->model->where('UserID', $userId) + ->where('DelDate', null) + ->first(); + + if (empty($user)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'User not found', + 'data' => null + ], 404); + } + + // Remove UserID from data array + unset($data['UserID']); + + // Don't allow updating these fields + unset($data['CreatedAt']); + unset($data['Username']); // Username should not change + + $data['UpdatedAt'] = date('Y-m-d H:i:s'); + + $updated = $this->model->update($userId, $data); + + if (!$updated) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to update user', + 'data' => null + ], 500); + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'User updated successfully', + 'data' => [ + 'UserID' => $userId, + 'updated_fields' => array_keys($data) + ] + ], 200); + + } catch (\Exception $e) { + log_message('error', 'UserController::update error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to update user', + 'data' => null + ], 500); + } + } + + /** + * Delete user (soft delete) + * DELETE /api/user/(:num) + */ + public function delete($id) + { + try { + // Check if user exists + $user = $this->model->where('UserID', $id) + ->where('DelDate', null) + ->first(); + + if (empty($user)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'User not found', + 'data' => null + ], 404); + } + + // Soft delete by setting DelDate + $deleted = $this->model->update($id, [ + 'DelDate' => date('Y-m-d H:i:s'), + 'IsActive' => false + ]); + + if (!$deleted) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to delete user', + 'data' => null + ], 500); + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'User deleted successfully', + 'data' => ['UserID' => $id] + ], 200); + + } catch (\Exception $e) { + log_message('error', 'UserController::delete error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to delete user', + 'data' => null + ], 500); + } + } +} diff --git a/app/Database/Migrations/2026-02-27-001500_CreateHostAppAndCodingSys.php b/app/Database/Migrations/2026-02-27-001500_CreateHostAppAndCodingSys.php index 366dd99..d2c304d 100755 --- a/app/Database/Migrations/2026-02-27-001500_CreateHostAppAndCodingSys.php +++ b/app/Database/Migrations/2026-02-27-001500_CreateHostAppAndCodingSys.php @@ -1,50 +1,50 @@ -forge->addField([ - 'HostAppID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], - 'HostAppName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false], - 'SiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => true], - 'CreateDate' => ['type' => 'DATETIME', 'null' => true], - 'EndDate' => ['type' => 'DATETIME', 'null' => true] - ]); - $this->forge->addKey('HostAppID', true); - $this->forge->createTable('hostapp'); - - // Table: hostcompara - $this->forge->addField([ - 'HostAppID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], - 'HostIP' => ['type' => 'VARCHAR', 'constraint' => 15, 'null' => true], - 'HostPort' => ['type' => 'VARCHAR', 'constraint' => 6, 'null' => true], - 'HostPwd' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], - 'CreateDate' => ['type' => 'DATETIME', 'null' => true], - 'EndDate' => ['type' => 'DATETIME', 'null' => true] - ]); - $this->forge->addKey('HostAppID', true); - $this->forge->createTable('hostcompara'); - - // Table: codingsys - $this->forge->addField([ - 'CodingSysID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], - 'CodingSysAbb' => ['type' => 'VARCHAR', 'constraint' => 6, 'null' => false, 'unique' => true], - 'FullText' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], - 'Description' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], - 'CreateDate' => ['type' => 'DATETIME', 'null' => true], - 'EndDate' => ['type' => 'DATETIME', 'null' => true] - ]); - $this->forge->addKey('CodingSysID', true); - $this->forge->createTable('codingsys'); - } - - public function down() { - $this->forge->dropTable('codingsys'); - $this->forge->dropTable('hostcompara'); - $this->forge->dropTable('hostapp'); - } -} +forge->addField([ + 'HostAppID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'HostAppName' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false], + 'SiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'CreateDate' => ['type' => 'DATETIME', 'null' => true], + 'EndDate' => ['type' => 'DATETIME', 'null' => true] + ]); + $this->forge->addKey('HostAppID', true); + $this->forge->createTable('hostapp'); + + // Table: hostcompara + $this->forge->addField([ + 'HostAppID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'HostIP' => ['type' => 'VARCHAR', 'constraint' => 15, 'null' => true], + 'HostPort' => ['type' => 'VARCHAR', 'constraint' => 6, 'null' => true], + 'HostPwd' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'CreateDate' => ['type' => 'DATETIME', 'null' => true], + 'EndDate' => ['type' => 'DATETIME', 'null' => true] + ]); + $this->forge->addKey('HostAppID', true); + $this->forge->createTable('hostcompara'); + + // Table: codingsys + $this->forge->addField([ + 'CodingSysID' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'CodingSysAbb' => ['type' => 'VARCHAR', 'constraint' => 6, 'null' => false, 'unique' => true], + 'FullText' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'Description' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'CreateDate' => ['type' => 'DATETIME', 'null' => true], + 'EndDate' => ['type' => 'DATETIME', 'null' => true] + ]); + $this->forge->addKey('CodingSysID', true); + $this->forge->createTable('codingsys'); + } + + public function down() { + $this->forge->dropTable('codingsys'); + $this->forge->dropTable('hostcompara'); + $this->forge->dropTable('hostapp'); + } +} diff --git a/app/Database/Migrations/2026-03-25-000050_CreateAuditLogs.php b/app/Database/Migrations/2026-03-25-000050_CreateAuditLogs.php index 6a3ea60..2645df9 100755 --- a/app/Database/Migrations/2026-03-25-000050_CreateAuditLogs.php +++ b/app/Database/Migrations/2026-03-25-000050_CreateAuditLogs.php @@ -1,137 +1,137 @@ - 'LogPatientID', - 'logorder' => 'LogOrderID', - 'logmaster' => 'LogMasterID', - 'logsystem' => 'LogSystemID', - ]; - - public function up(): void - { - foreach ($this->logTables as $table => $pk) { - $this->createLogTable($table, $pk); - } - } - - public function down(): void - { - foreach (array_reverse($this->logTables) as $table => $pk) { - $this->forge->dropTable($table, true); - } - } - - private function createLogTable(string $table, string $primaryKey): void - { - $fields = [ - $primaryKey => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'TblName' => [ - 'type' => 'VARCHAR', - 'constraint' => 64, - ], - 'RecID' => [ - 'type' => 'VARCHAR', - 'constraint' => 64, - ], - 'FldName' => [ - 'type' => 'VARCHAR', - 'constraint' => 128, - 'null' => true, - ], - 'FldValuePrev' => [ - 'type' => 'TEXT', - 'null' => true, - ], - 'FldValueNew' => [ - 'type' => 'TEXT', - 'null' => true, - ], - 'UserID' => [ - 'type' => 'VARCHAR', - 'constraint' => 64, - ], - 'SiteID' => [ - 'type' => 'VARCHAR', - 'constraint' => 32, - ], - 'DIDType' => [ - 'type' => 'VARCHAR', - 'constraint' => 32, - 'null' => true, - ], - 'DID' => [ - 'type' => 'VARCHAR', - 'constraint' => 128, - 'null' => true, - ], - 'MachineID' => [ - 'type' => 'VARCHAR', - 'constraint' => 128, - 'null' => true, - ], - 'SessionID' => [ - 'type' => 'VARCHAR', - 'constraint' => 128, - ], - 'AppID' => [ - 'type' => 'VARCHAR', - 'constraint' => 64, - ], - 'ProcessID' => [ - 'type' => 'VARCHAR', - 'constraint' => 128, - 'null' => true, - ], - 'WebPageID' => [ - 'type' => 'VARCHAR', - 'constraint' => 128, - 'null' => true, - ], - 'EventID' => [ - 'type' => 'VARCHAR', - 'constraint' => 80, - ], - 'ActivityID' => [ - 'type' => 'VARCHAR', - 'constraint' => 24, - ], - 'Reason' => [ - 'type' => 'VARCHAR', - 'constraint' => 512, - 'null' => true, - ], - 'LogDate' => [ - 'type' => 'DATETIME', - 'constraint' => 3, - ], - 'Context' => [ - 'type' => 'JSON', - ], - 'IpAddress' => [ - 'type' => 'VARCHAR', - 'constraint' => 45, - 'null' => true, - ], - ]; - - $this->forge->addField($fields); - $this->forge->addKey($primaryKey, true); - $this->forge->addKey(['LogDate'], false, false, "idx_{$table}_logdate"); - $this->forge->addKey(['RecID', 'LogDate'], false, false, "idx_{$table}_recid_logdate"); - $this->forge->addKey(['UserID', 'LogDate'], false, false, "idx_{$table}_userid_logdate"); - $this->forge->addKey(['EventID', 'LogDate'], false, false, "idx_{$table}_eventid_logdate"); - $this->forge->addKey(['SiteID', 'LogDate'], false, false, "idx_{$table}_site_logdate"); - $this->forge->createTable($table, true); - } -} + 'LogPatientID', + 'logorder' => 'LogOrderID', + 'logmaster' => 'LogMasterID', + 'logsystem' => 'LogSystemID', + ]; + + public function up(): void + { + foreach ($this->logTables as $table => $pk) { + $this->createLogTable($table, $pk); + } + } + + public function down(): void + { + foreach (array_reverse($this->logTables) as $table => $pk) { + $this->forge->dropTable($table, true); + } + } + + private function createLogTable(string $table, string $primaryKey): void + { + $fields = [ + $primaryKey => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'TblName' => [ + 'type' => 'VARCHAR', + 'constraint' => 64, + ], + 'RecID' => [ + 'type' => 'VARCHAR', + 'constraint' => 64, + ], + 'FldName' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => true, + ], + 'FldValuePrev' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'FldValueNew' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'UserID' => [ + 'type' => 'VARCHAR', + 'constraint' => 64, + ], + 'SiteID' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + ], + 'DIDType' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + 'null' => true, + ], + 'DID' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => true, + ], + 'MachineID' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => true, + ], + 'SessionID' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + ], + 'AppID' => [ + 'type' => 'VARCHAR', + 'constraint' => 64, + ], + 'ProcessID' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => true, + ], + 'WebPageID' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => true, + ], + 'EventID' => [ + 'type' => 'VARCHAR', + 'constraint' => 80, + ], + 'ActivityID' => [ + 'type' => 'VARCHAR', + 'constraint' => 24, + ], + 'Reason' => [ + 'type' => 'VARCHAR', + 'constraint' => 512, + 'null' => true, + ], + 'LogDate' => [ + 'type' => 'DATETIME', + 'constraint' => 3, + ], + 'Context' => [ + 'type' => 'JSON', + ], + 'IpAddress' => [ + 'type' => 'VARCHAR', + 'constraint' => 45, + 'null' => true, + ], + ]; + + $this->forge->addField($fields); + $this->forge->addKey($primaryKey, true); + $this->forge->addKey(['LogDate'], false, false, "idx_{$table}_logdate"); + $this->forge->addKey(['RecID', 'LogDate'], false, false, "idx_{$table}_recid_logdate"); + $this->forge->addKey(['UserID', 'LogDate'], false, false, "idx_{$table}_userid_logdate"); + $this->forge->addKey(['EventID', 'LogDate'], false, false, "idx_{$table}_eventid_logdate"); + $this->forge->addKey(['SiteID', 'LogDate'], false, false, "idx_{$table}_site_logdate"); + $this->forge->createTable($table, true); + } +} diff --git a/app/Database/Seeds/HostAppCodingSysSeeder.php b/app/Database/Seeds/HostAppCodingSysSeeder.php index 67d8d5c..968fd5e 100755 --- a/app/Database/Seeds/HostAppCodingSysSeeder.php +++ b/app/Database/Seeds/HostAppCodingSysSeeder.php @@ -1,222 +1,222 @@ -db->table('hostcompara')->emptyTable(); - $this->db->table('hostapp')->emptyTable(); - $this->db->table('codingsys')->emptyTable(); - - // HostApp - Host Applications - $hostAppData = [ - [ - 'HostAppID' => 1, - 'HostAppName' => 'Laboratory Information System Main', - 'SiteID' => 1, - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 2, - 'HostAppName' => 'Laboratory Information System Backup', - 'SiteID' => 1, - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 3, - 'HostAppName' => 'Electronic Medical Record System', - 'SiteID' => 1, - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 4, - 'HostAppName' => 'Picture Archiving System', - 'SiteID' => 1, - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 5, - 'HostAppName' => 'Billing System', - 'SiteID' => 1, - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 6, - 'HostAppName' => 'Insurance System Integration', - 'SiteID' => 1, - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 7, - 'HostAppName' => 'Legacy Laboratory System', - 'SiteID' => 1, - 'CreateDate' => $now, - 'EndDate' => date('Y-m-d H:i:s', strtotime('-1 year')) - ], - ]; - $this->db->table('hostapp')->insertBatch($hostAppData); - - // HostComPara - Host Communication Parameters - $hostComParaData = [ - [ - 'HostAppID' => 1, - 'HostIP' => '192.168.1.10', - 'HostPort' => '8080', - 'HostPwd' => 'lis_main_pass_2024', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 2, - 'HostIP' => '192.168.1.11', - 'HostPort' => '8081', - 'HostPwd' => 'lis_backup_pass_2024', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 3, - 'HostIP' => '192.168.1.20', - 'HostPort' => '443', - 'HostPwd' => 'emr_secure_2024', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 4, - 'HostIP' => '192.168.1.30', - 'HostPort' => '8042', - 'HostPwd' => 'pacs_dicom_2024', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 5, - 'HostIP' => '192.168.1.40', - 'HostPort' => '8443', - 'HostPwd' => 'bill_payment_2024', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 6, - 'HostIP' => '192.168.1.50', - 'HostPort' => '443', - 'HostPwd' => 'ins_claim_2024', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'HostAppID' => 7, - 'HostIP' => '192.168.1.99', - 'HostPort' => '8080', - 'HostPwd' => 'old_legacy_pass', - 'CreateDate' => $now, - 'EndDate' => date('Y-m-d H:i:s', strtotime('-1 year')) - ], - ]; - $this->db->table('hostcompara')->insertBatch($hostComParaData); - - // CodingSys - Coding Systems - $codingSysData = [ - [ - 'CodingSysAbb' => 'ICD10', - 'FullText' => 'International Classification of Diseases 10th Revision', - 'Description' => 'Medical diagnosis coding system for diseases and health conditions', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'ICD10PCS', - 'FullText' => 'ICD-10 Procedure Coding System', - 'Description' => 'Classification system for inpatient hospital procedures', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'LOINC', - 'FullText' => 'Logical Observation Identifiers Names and Codes', - 'Description' => 'Standard for identifying medical laboratory observations', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'SNOMED', - 'FullText' => 'SNOMED CT', - 'Description' => 'Systematized Nomenclature of Medicine - Clinical Terms', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'CPT', - 'FullText' => 'Current Procedural Terminology', - 'Description' => 'Medical code set for medical procedures and services', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'HCPCS', - 'FullText' => 'Healthcare Common Procedure Coding System', - 'Description' => 'Medical code set for equipment, supplies, and services', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'RXNORM', - 'FullText' => 'RxNorm', - 'Description' => 'Normalized naming system for medications', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'NDC', - 'FullText' => 'National Drug Code', - 'Description' => 'Unique identifier for human drugs in the United States', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'UCUM', - 'FullText' => 'Unified Code for Units of Measure', - 'Description' => 'Standard for units of measurement in clinical and scientific contexts', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'CVX', - 'FullText' => 'Vaccines Administered', - 'Description' => 'Vaccine codes for immunization records', - 'CreateDate' => $now, - 'EndDate' => null - ], - [ - 'CodingSysAbb' => 'ICD9', - 'FullText' => 'International Classification of Diseases 9th Revision', - 'Description' => 'Legacy medical diagnosis coding system', - 'CreateDate' => $now, - 'EndDate' => date('Y-m-d H:i:s', strtotime('-2 years')) - ], - [ - 'CodingSysAbb' => 'ICD9CM', - 'FullText' => 'ICD-9-CM', - 'Description' => 'Legacy procedure coding system', - 'CreateDate' => $now, - 'EndDate' => date('Y-m-d H:i:s', strtotime('-2 years')) - ], - ]; - $this->db->table('codingsys')->insertBatch($codingSysData); - } -} +db->table('hostcompara')->emptyTable(); + $this->db->table('hostapp')->emptyTable(); + $this->db->table('codingsys')->emptyTable(); + + // HostApp - Host Applications + $hostAppData = [ + [ + 'HostAppID' => 1, + 'HostAppName' => 'Laboratory Information System Main', + 'SiteID' => 1, + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 2, + 'HostAppName' => 'Laboratory Information System Backup', + 'SiteID' => 1, + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 3, + 'HostAppName' => 'Electronic Medical Record System', + 'SiteID' => 1, + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 4, + 'HostAppName' => 'Picture Archiving System', + 'SiteID' => 1, + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 5, + 'HostAppName' => 'Billing System', + 'SiteID' => 1, + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 6, + 'HostAppName' => 'Insurance System Integration', + 'SiteID' => 1, + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 7, + 'HostAppName' => 'Legacy Laboratory System', + 'SiteID' => 1, + 'CreateDate' => $now, + 'EndDate' => date('Y-m-d H:i:s', strtotime('-1 year')) + ], + ]; + $this->db->table('hostapp')->insertBatch($hostAppData); + + // HostComPara - Host Communication Parameters + $hostComParaData = [ + [ + 'HostAppID' => 1, + 'HostIP' => '192.168.1.10', + 'HostPort' => '8080', + 'HostPwd' => 'lis_main_pass_2024', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 2, + 'HostIP' => '192.168.1.11', + 'HostPort' => '8081', + 'HostPwd' => 'lis_backup_pass_2024', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 3, + 'HostIP' => '192.168.1.20', + 'HostPort' => '443', + 'HostPwd' => 'emr_secure_2024', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 4, + 'HostIP' => '192.168.1.30', + 'HostPort' => '8042', + 'HostPwd' => 'pacs_dicom_2024', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 5, + 'HostIP' => '192.168.1.40', + 'HostPort' => '8443', + 'HostPwd' => 'bill_payment_2024', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 6, + 'HostIP' => '192.168.1.50', + 'HostPort' => '443', + 'HostPwd' => 'ins_claim_2024', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'HostAppID' => 7, + 'HostIP' => '192.168.1.99', + 'HostPort' => '8080', + 'HostPwd' => 'old_legacy_pass', + 'CreateDate' => $now, + 'EndDate' => date('Y-m-d H:i:s', strtotime('-1 year')) + ], + ]; + $this->db->table('hostcompara')->insertBatch($hostComParaData); + + // CodingSys - Coding Systems + $codingSysData = [ + [ + 'CodingSysAbb' => 'ICD10', + 'FullText' => 'International Classification of Diseases 10th Revision', + 'Description' => 'Medical diagnosis coding system for diseases and health conditions', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'ICD10PCS', + 'FullText' => 'ICD-10 Procedure Coding System', + 'Description' => 'Classification system for inpatient hospital procedures', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'LOINC', + 'FullText' => 'Logical Observation Identifiers Names and Codes', + 'Description' => 'Standard for identifying medical laboratory observations', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'SNOMED', + 'FullText' => 'SNOMED CT', + 'Description' => 'Systematized Nomenclature of Medicine - Clinical Terms', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'CPT', + 'FullText' => 'Current Procedural Terminology', + 'Description' => 'Medical code set for medical procedures and services', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'HCPCS', + 'FullText' => 'Healthcare Common Procedure Coding System', + 'Description' => 'Medical code set for equipment, supplies, and services', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'RXNORM', + 'FullText' => 'RxNorm', + 'Description' => 'Normalized naming system for medications', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'NDC', + 'FullText' => 'National Drug Code', + 'Description' => 'Unique identifier for human drugs in the United States', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'UCUM', + 'FullText' => 'Unified Code for Units of Measure', + 'Description' => 'Standard for units of measurement in clinical and scientific contexts', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'CVX', + 'FullText' => 'Vaccines Administered', + 'Description' => 'Vaccine codes for immunization records', + 'CreateDate' => $now, + 'EndDate' => null + ], + [ + 'CodingSysAbb' => 'ICD9', + 'FullText' => 'International Classification of Diseases 9th Revision', + 'Description' => 'Legacy medical diagnosis coding system', + 'CreateDate' => $now, + 'EndDate' => date('Y-m-d H:i:s', strtotime('-2 years')) + ], + [ + 'CodingSysAbb' => 'ICD9CM', + 'FullText' => 'ICD-9-CM', + 'Description' => 'Legacy procedure coding system', + 'CreateDate' => $now, + 'EndDate' => date('Y-m-d H:i:s', strtotime('-2 years')) + ], + ]; + $this->db->table('codingsys')->insertBatch($codingSysData); + } +} diff --git a/app/Libraries/Data/activity_result.json b/app/Libraries/Data/activity_result.json index 22c3c08..2d55303 100755 --- a/app/Libraries/Data/activity_result.json +++ b/app/Libraries/Data/activity_result.json @@ -1,9 +1,9 @@ -{"name": "activity_result", - "VSName": "Activity Result", - "VCategory": "System", - "values": [ - {"key": "0", "value": "Failed"}, - {"key": "1", "value": "Success with note"}, - {"key": "2", "value": "Success"} - ] -} +{"name": "activity_result", + "VSName": "Activity Result", + "VCategory": "System", + "values": [ + {"key": "0", "value": "Failed"}, + {"key": "1", "value": "Success with note"}, + {"key": "2", "value": "Success"} + ] +} diff --git a/app/Libraries/Data/additive.json b/app/Libraries/Data/additive.json index b4a04f4..ea31cc9 100755 --- a/app/Libraries/Data/additive.json +++ b/app/Libraries/Data/additive.json @@ -1,25 +1,25 @@ -{"name": "additive", - "VSName": "Additive", - "VCategory": "System", - "values": [ - {"key": "Hep", "value": "Heparin ammonium"}, - {"key": "Apro", "value": "Aprotinin"}, - {"key": "HepCa", "value": "Heparin calcium"}, - {"key": "H3BO3", "value": "Boric acid"}, - {"key": "CaOxa", "value": "Calcium oxalate"}, - {"key": "EDTA", "value": "EDTA"}, - {"key": "Ede", "value": "Edetate"}, - {"key": "HCl", "value": "Hydrochloric acid"}, - {"key": "Hrdn", "value": "Hirudin"}, - {"key": "EdeK", "value": "Edetate dipotassium"}, - {"key": "EdeTri", "value": "Tripotassium edetate"}, - {"key": "LiHep", "value": "Heparin lithium"}, - {"key": "EdeNa", "value": "Edetate disodium"}, - {"key": "NaCtrt", "value": "Sodium citrate"}, - {"key": "NaHep", "value": "Heparin sodium"}, - {"key": "NaF", "value": "Sodium fluoride"}, - {"key": "Borax", "value": "Sodium tetraborate"}, - {"key": "Mntl", "value": "Mannitol"}, - {"key": "NaFrm", "value": "Sodium formate"} - ] -} +{"name": "additive", + "VSName": "Additive", + "VCategory": "System", + "values": [ + {"key": "Hep", "value": "Heparin ammonium"}, + {"key": "Apro", "value": "Aprotinin"}, + {"key": "HepCa", "value": "Heparin calcium"}, + {"key": "H3BO3", "value": "Boric acid"}, + {"key": "CaOxa", "value": "Calcium oxalate"}, + {"key": "EDTA", "value": "EDTA"}, + {"key": "Ede", "value": "Edetate"}, + {"key": "HCl", "value": "Hydrochloric acid"}, + {"key": "Hrdn", "value": "Hirudin"}, + {"key": "EdeK", "value": "Edetate dipotassium"}, + {"key": "EdeTri", "value": "Tripotassium edetate"}, + {"key": "LiHep", "value": "Heparin lithium"}, + {"key": "EdeNa", "value": "Edetate disodium"}, + {"key": "NaCtrt", "value": "Sodium citrate"}, + {"key": "NaHep", "value": "Heparin sodium"}, + {"key": "NaF", "value": "Sodium fluoride"}, + {"key": "Borax", "value": "Sodium tetraborate"}, + {"key": "Mntl", "value": "Mannitol"}, + {"key": "NaFrm", "value": "Sodium formate"} + ] +} diff --git a/app/Libraries/Data/area_class.json b/app/Libraries/Data/area_class.json index dd6c9bb..b542664 100755 --- a/app/Libraries/Data/area_class.json +++ b/app/Libraries/Data/area_class.json @@ -1,9 +1,9 @@ -{"name": "area_class", - "VSName": "Area Class", - "VCategory": "System", - "values": [ - {"key": "PROP", "value": "Propinsi"}, - {"key": "KAB", "value": "Kabupaten"}, - {"key": "KOTA", "value": "Kota"} - ] -} +{"name": "area_class", + "VSName": "Area Class", + "VCategory": "System", + "values": [ + {"key": "PROP", "value": "Propinsi"}, + {"key": "KAB", "value": "Kabupaten"}, + {"key": "KOTA", "value": "Kota"} + ] +} diff --git a/app/Libraries/Data/body_site.json b/app/Libraries/Data/body_site.json index abb4b69..0bdbadf 100755 --- a/app/Libraries/Data/body_site.json +++ b/app/Libraries/Data/body_site.json @@ -1,10 +1,10 @@ -{"name": "body_site", - "VSName": "Body Site", - "VCategory": "System", - "values": [ - {"key": "LA", "value": "Left Arm"}, - {"key": "RA", "value": "Right Arm"}, - {"key": "LF", "value": "Left Foot"}, - {"key": "RF", "value": "Right Foot"} - ] -} +{"name": "body_site", + "VSName": "Body Site", + "VCategory": "System", + "values": [ + {"key": "LA", "value": "Left Arm"}, + {"key": "RA", "value": "Right Arm"}, + {"key": "LF", "value": "Left Foot"}, + {"key": "RF", "value": "Right Foot"} + ] +} diff --git a/app/Libraries/Data/collection_method.json b/app/Libraries/Data/collection_method.json index 3bed5eb..b00ed5c 100755 --- a/app/Libraries/Data/collection_method.json +++ b/app/Libraries/Data/collection_method.json @@ -1,16 +1,16 @@ -{"name": "collection_method", - "VSName": "Collection Method", - "VCategory": "System", - "values": [ - {"key": "pcntr", "value": "Puncture"}, - {"key": "fprk", "value": "Finger-prick sampling"}, - {"key": "ucct", "value": "Urine specimen collection, clean catch"}, - {"key": "utcl", "value": "Timed urine collection"}, - {"key": "ucth", "value": "Urine specimen collection, catheterized"}, - {"key": "scgh", "value": "Collection of coughed sputum"}, - {"key": "bpsy", "value": "Biopsy"}, - {"key": "aspn", "value": "Aspiration"}, - {"key": "excs", "value": "Excision"}, - {"key": "scrp", "value": "Scraping"} - ] -} +{"name": "collection_method", + "VSName": "Collection Method", + "VCategory": "System", + "values": [ + {"key": "pcntr", "value": "Puncture"}, + {"key": "fprk", "value": "Finger-prick sampling"}, + {"key": "ucct", "value": "Urine specimen collection, clean catch"}, + {"key": "utcl", "value": "Timed urine collection"}, + {"key": "ucth", "value": "Urine specimen collection, catheterized"}, + {"key": "scgh", "value": "Collection of coughed sputum"}, + {"key": "bpsy", "value": "Biopsy"}, + {"key": "aspn", "value": "Aspiration"}, + {"key": "excs", "value": "Excision"}, + {"key": "scrp", "value": "Scraping"} + ] +} diff --git a/app/Libraries/Data/container_cap_color.json b/app/Libraries/Data/container_cap_color.json index 2d60188..24ff943 100755 --- a/app/Libraries/Data/container_cap_color.json +++ b/app/Libraries/Data/container_cap_color.json @@ -1,14 +1,14 @@ -{"name": "container_cap_color", - "VSName": "Container Cap Color", - "VCategory": "System", - "values": [ - {"key": "PRPL", "value": "Purple"}, - {"key": "RED", "value": "Red"}, - {"key": "YLLW", "value": "Yellow"}, - {"key": "GRN", "value": "Green"}, - {"key": "PINK", "value": "Pink"}, - {"key": "LBLU", "value": "Light Blue"}, - {"key": "RBLU", "value": "Royal Blue"}, - {"key": "GRAY", "value": "Gray"} - ] -} +{"name": "container_cap_color", + "VSName": "Container Cap Color", + "VCategory": "System", + "values": [ + {"key": "PRPL", "value": "Purple"}, + {"key": "RED", "value": "Red"}, + {"key": "YLLW", "value": "Yellow"}, + {"key": "GRN", "value": "Green"}, + {"key": "PINK", "value": "Pink"}, + {"key": "LBLU", "value": "Light Blue"}, + {"key": "RBLU", "value": "Royal Blue"}, + {"key": "GRAY", "value": "Gray"} + ] +} diff --git a/app/Libraries/Data/container_class.json b/app/Libraries/Data/container_class.json index d6627a4..7c6abee 100755 --- a/app/Libraries/Data/container_class.json +++ b/app/Libraries/Data/container_class.json @@ -1,9 +1,9 @@ -{"name": "container_class", - "VSName": "Container Class", - "VCategory": "System", - "values": [ - {"key": "Pri", "value": "Primary"}, - {"key": "Sec", "value": "Secondary"}, - {"key": "Ter", "value": "Tertiary"} - ] -} +{"name": "container_class", + "VSName": "Container Class", + "VCategory": "System", + "values": [ + {"key": "Pri", "value": "Primary"}, + {"key": "Sec", "value": "Secondary"}, + {"key": "Ter", "value": "Tertiary"} + ] +} diff --git a/app/Libraries/Data/container_size.json b/app/Libraries/Data/container_size.json index b45ffed..1daaabc 100755 --- a/app/Libraries/Data/container_size.json +++ b/app/Libraries/Data/container_size.json @@ -1,10 +1,10 @@ -{"name": "container_size", - "VSName": "Container Size", - "VCategory": "System", - "values": [ - {"key": "5ml", "value": "5 mL"}, - {"key": "7ml", "value": "7 mL"}, - {"key": "10ml", "value": "10 mL"}, - {"key": "1l", "value": "1 L"} - ] -} +{"name": "container_size", + "VSName": "Container Size", + "VCategory": "System", + "values": [ + {"key": "5ml", "value": "5 mL"}, + {"key": "7ml", "value": "7 mL"}, + {"key": "10ml", "value": "10 mL"}, + {"key": "1l", "value": "1 L"} + ] +} diff --git a/app/Libraries/Data/country.json b/app/Libraries/Data/country.json index 015e850..ef7d7c7 100755 --- a/app/Libraries/Data/country.json +++ b/app/Libraries/Data/country.json @@ -1,255 +1,255 @@ -{"name": "country", - "VSName": "Country", - "VCategory": "System", - "values": [ - {"key": "AFG", "value": "Afghanistan"}, - {"key": "ALA", "value": "Åland Islands"}, - {"key": "ALB", "value": "Albania"}, - {"key": "DZA", "value": "Algeria"}, - {"key": "ASM", "value": "American Samoa"}, - {"key": "AND", "value": "Andorra"}, - {"key": "AGO", "value": "Angola"}, - {"key": "AIA", "value": "Anguilla"}, - {"key": "ATA", "value": "Antarctica"}, - {"key": "ATG", "value": "Antigua and Barbuda"}, - {"key": "ARG", "value": "Argentina"}, - {"key": "ARM", "value": "Armenia"}, - {"key": "ABW", "value": "Aruba"}, - {"key": "AUS", "value": "Australia"}, - {"key": "AUT", "value": "Austria"}, - {"key": "AZE", "value": "Azerbaijan"}, - {"key": "BHS", "value": "Bahamas"}, - {"key": "BHR", "value": "Bahrain"}, - {"key": "BGD", "value": "Bangladesh"}, - {"key": "BRB", "value": "Barbados"}, - {"key": "BLR", "value": "Belarus"}, - {"key": "BEL", "value": "Belgium"}, - {"key": "BLZ", "value": "Belize"}, - {"key": "BEN", "value": "Benin"}, - {"key": "BMU", "value": "Bermuda"}, - {"key": "BTN", "value": "Bhutan"}, - {"key": "BOL", "value": "Bolivia, Plurinational State of"}, - {"key": "BES", "value": "Bonaire, Sint Eustatius and Saba"}, - {"key": "BIH", "value": "Bosnia and Herzegovina"}, - {"key": "BWA", "value": "Botswana"}, - {"key": "BVT", "value": "Bouvet Island"}, - {"key": "BRA", "value": "Brazil"}, - {"key": "IOT", "value": "British Indian Ocean Territory"}, - {"key": "BRN", "value": "Brunei Darussalam"}, - {"key": "BGR", "value": "Bulgaria"}, - {"key": "BFA", "value": "Burkina Faso"}, - {"key": "BDI", "value": "Burundi"}, - {"key": "CPV", "value": "Cabo Verde"}, - {"key": "KHM", "value": "Cambodia"}, - {"key": "CMR", "value": "Cameroon"}, - {"key": "CAN", "value": "Canada"}, - {"key": "CYM", "value": "Cayman Islands"}, - {"key": "CAF", "value": "Central African Republic"}, - {"key": "TCD", "value": "Chad"}, - {"key": "CHL", "value": "Chile"}, - {"key": "CHN", "value": "China"}, - {"key": "CXR", "value": "Christmas Island"}, - {"key": "CCK", "value": "Cocos (Keeling) Islands"}, - {"key": "COL", "value": "Colombia"}, - {"key": "COM", "value": "Comoros"}, - {"key": "COG", "value": "Congo"}, - {"key": "COD", "value": "Congo, Democratic Republic of the"}, - {"key": "COK", "value": "Cook Islands"}, - {"key": "CRI", "value": "Costa Rica"}, - {"key": "CIV", "value": "Côte d'Ivoire"}, - {"key": "HRV", "value": "Croatia"}, - {"key": "CUB", "value": "Cuba"}, - {"key": "CUW", "value": "Curaçao"}, - {"key": "CYP", "value": "Cyprus"}, - {"key": "CZE", "value": "Czechia"}, - {"key": "DNK", "value": "Denmark"}, - {"key": "DJI", "value": "Djibouti"}, - {"key": "DMA", "value": "Dominica"}, - {"key": "DOM", "value": "Dominican Republic"}, - {"key": "ECU", "value": "Ecuador"}, - {"key": "EGY", "value": "Egypt"}, - {"key": "SLV", "value": "El Salvador"}, - {"key": "GNQ", "value": "Equatorial Guinea"}, - {"key": "ERI", "value": "Eritrea"}, - {"key": "EST", "value": "Estonia"}, - {"key": "SWZ", "value": "Eswatini"}, - {"key": "ETH", "value": "Ethiopia"}, - {"key": "FLK", "value": "Falkland Islands (Malvinas)"}, - {"key": "FRO", "value": "Faroe Islands"}, - {"key": "FJI", "value": "Fiji"}, - {"key": "FIN", "value": "Finland"}, - {"key": "FRA", "value": "France"}, - {"key": "GUF", "value": "French Guiana"}, - {"key": "PYF", "value": "French Polynesia"}, - {"key": "ATF", "value": "French Southern Territories"}, - {"key": "GAB", "value": "Gabon"}, - {"key": "GMB", "value": "Gambia"}, - {"key": "GEO", "value": "Georgia"}, - {"key": "DEU", "value": "Germany"}, - {"key": "GHA", "value": "Ghana"}, - {"key": "GIB", "value": "Gibraltar"}, - {"key": "GRC", "value": "Greece"}, - {"key": "GRL", "value": "Greenland"}, - {"key": "GRD", "value": "Grenada"}, - {"key": "GLP", "value": "Guadeloupe"}, - {"key": "GUM", "value": "Guam"}, - {"key": "GTM", "value": "Guatemala"}, - {"key": "GGY", "value": "Guernsey"}, - {"key": "GIN", "value": "Guinea"}, - {"key": "GNB", "value": "Guinea-Bissau"}, - {"key": "GUY", "value": "Guyana"}, - {"key": "HTI", "value": "Haiti"}, - {"key": "HMD", "value": "Heard Island and McDonald Islands"}, - {"key": "VAT", "value": "Holy See"}, - {"key": "HND", "value": "Honduras"}, - {"key": "HKG", "value": "Hong Kong"}, - {"key": "HUN", "value": "Hungary"}, - {"key": "ISL", "value": "Iceland"}, - {"key": "IND", "value": "India"}, - {"key": "IDN", "value": "Indonesia"}, - {"key": "IRN", "value": "Iran, Islamic Republic of"}, - {"key": "IRQ", "value": "Iraq"}, - {"key": "IRL", "value": "Ireland"}, - {"key": "IMN", "value": "Isle of Man"}, - {"key": "ISR", "value": "Israel"}, - {"key": "ITA", "value": "Italy"}, - {"key": "JAM", "value": "Jamaica"}, - {"key": "JPN", "value": "Japan"}, - {"key": "JEY", "value": "Jersey"}, - {"key": "JOR", "value": "Jordan"}, - {"key": "KAZ", "value": "Kazakhstan"}, - {"key": "KEN", "value": "Kenya"}, - {"key": "KIR", "value": "Kiribati"}, - {"key": "PRK", "value": "Korea, Democratic People's Republic of"}, - {"key": "KOR", "value": "Korea, Republic of"}, - {"key": "KWT", "value": "Kuwait"}, - {"key": "KGZ", "value": "Kyrgyzstan"}, - {"key": "LAO", "value": "Lao People's Democratic Republic"}, - {"key": "LVA", "value": "Latvia"}, - {"key": "LBN", "value": "Lebanon"}, - {"key": "LSO", "value": "Lesotho"}, - {"key": "LBR", "value": "Liberia"}, - {"key": "LBY", "value": "Libya"}, - {"key": "LIE", "value": "Liechtenstein"}, - {"key": "LTU", "value": "Lithuania"}, - {"key": "LUX", "value": "Luxembourg"}, - {"key": "MAC", "value": "Macao"}, - {"key": "MDG", "value": "Madagascar"}, - {"key": "MWI", "value": "Malawi"}, - {"key": "MYS", "value": "Malaysia"}, - {"key": "MDV", "value": "Maldives"}, - {"key": "MLI", "value": "Mali"}, - {"key": "MLT", "value": "Malta"}, - {"key": "MHL", "value": "Marshall Islands"}, - {"key": "MTQ", "value": "Martinique"}, - {"key": "MRT", "value": "Mauritania"}, - {"key": "MUS", "value": "Mauritius"}, - {"key": "MYT", "value": "Mayotte"}, - {"key": "MEX", "value": "Mexico"}, - {"key": "FSM", "value": "Micronesia, Federated States of"}, - {"key": "MDA", "value": "Moldova, Republic of"}, - {"key": "MCO", "value": "Monaco"}, - {"key": "MNG", "value": "Mongolia"}, - {"key": "MNE", "value": "Montenegro"}, - {"key": "MSR", "value": "Montserrat"}, - {"key": "MAR", "value": "Morocco"}, - {"key": "MOZ", "value": "Mozambique"}, - {"key": "MMR", "value": "Myanmar"}, - {"key": "NAM", "value": "Namibia"}, - {"key": "NRU", "value": "Nauru"}, - {"key": "NPL", "value": "Nepal"}, - {"key": "NLD", "value": "Netherlands, Kingdom of the"}, - {"key": "NCL", "value": "New Caledonia"}, - {"key": "NZL", "value": "New Zealand"}, - {"key": "NIC", "value": "Nicaragua"}, - {"key": "NER", "value": "Niger"}, - {"key": "NGA", "value": "Nigeria"}, - {"key": "NIU", "value": "Niue"}, - {"key": "NFK", "value": "Norfolk Island"}, - {"key": "MKD", "value": "North Macedonia"}, - {"key": "MNP", "value": "Northern Mariana Islands"}, - {"key": "NOR", "value": "Norway"}, - {"key": "OMN", "value": "Oman"}, - {"key": "PAK", "value": "Pakistan"}, - {"key": "PLW", "value": "Palau"}, - {"key": "PSE", "value": "Palestine, State of"}, - {"key": "PAN", "value": "Panama"}, - {"key": "PNG", "value": "Papua New Guinea"}, - {"key": "PRY", "value": "Paraguay"}, - {"key": "PER", "value": "Peru"}, - {"key": "PHL", "value": "Philippines"}, - {"key": "PCN", "value": "Pitcairn"}, - {"key": "POL", "value": "Poland"}, - {"key": "PRT", "value": "Portugal"}, - {"key": "PRI", "value": "Puerto Rico"}, - {"key": "QAT", "value": "Qatar"}, - {"key": "REU", "value": "Réunion"}, - {"key": "ROU", "value": "Romania"}, - {"key": "RUS", "value": "Russian Federation"}, - {"key": "RWA", "value": "Rwanda"}, - {"key": "BLM", "value": "Saint Barthélemy"}, - {"key": "SHN", "value": "Saint Helena, Ascension and Tristan da Cunha"}, - {"key": "KNA", "value": "Saint Kitts and Nevis"}, - {"key": "LCA", "value": "Saint Lucia"}, - {"key": "MAF", "value": "Saint Martin (French part)"}, - {"key": "SPM", "value": "Saint Pierre and Miquelon"}, - {"key": "VCT", "value": "Saint Vincent and the Grenadines"}, - {"key": "WSM", "value": "Samoa"}, - {"key": "SMR", "value": "San Marino"}, - {"key": "STP", "value": "Sao Tome and Principe"}, - {"key": "SAU", "value": "Saudi Arabia"}, - {"key": "SEN", "value": "Senegal"}, - {"key": "SRB", "value": "Serbia"}, - {"key": "SYC", "value": "Seychelles"}, - {"key": "SLE", "value": "Sierra Leone"}, - {"key": "SGP", "value": "Singapore"}, - {"key": "SXM", "value": "Sint Maarten (Dutch part)"}, - {"key": "SVK", "value": "Slovakia"}, - {"key": "SVN", "value": "Slovenia"}, - {"key": "SLB", "value": "Solomon Islands"}, - {"key": "SOM", "value": "Somalia"}, - {"key": "ZAF", "value": "South Africa"}, - {"key": "SGS", "value": "South Georgia and the South Sandwich Islands"}, - {"key": "SSD", "value": "South Sudan"}, - {"key": "ESP", "value": "Spain"}, - {"key": "LKA", "value": "Sri Lanka"}, - {"key": "SDN", "value": "Sudan"}, - {"key": "SUR", "value": "Suriname"}, - {"key": "SJM", "value": "Svalbard and Jan Mayen"}, - {"key": "SWE", "value": "Sweden"}, - {"key": "CHE", "value": "Switzerland"}, - {"key": "SYR", "value": "Syrian Arab Republic"}, - {"key": "TWN", "value": "Taiwan, Province of China"}, - {"key": "TJK", "value": "Tajikistan"}, - {"key": "TZA", "value": "Tanzania, United Republic of"}, - {"key": "THA", "value": "Thailand"}, - {"key": "TLS", "value": "Timor-Leste"}, - {"key": "TGO", "value": "Togo"}, - {"key": "TKL", "value": "Tokelau"}, - {"key": "TON", "value": "Tonga"}, - {"key": "TTO", "value": "Trinidad and Tobago"}, - {"key": "TUN", "value": "Tunisia"}, - {"key": "TUR", "value": "Türkiye"}, - {"key": "TKM", "value": "Turkmenistan"}, - {"key": "TCA", "value": "Turks and Caicos Islands"}, - {"key": "TUV", "value": "Tuvalu"}, - {"key": "UGA", "value": "Uganda"}, - {"key": "UKR", "value": "Ukraine"}, - {"key": "ARE", "value": "United Arab Emirates"}, - {"key": "GBR", "value": "United Kingdom of Great Britain and Northern Ireland"}, - {"key": "USA", "value": "United States of America"}, - {"key": "UMI", "value": "United States Minor Outlying Islands"}, - {"key": "URY", "value": "Uruguay"}, - {"key": "UZB", "value": "Uzbekistan"}, - {"key": "VUT", "value": "Vanuatu"}, - {"key": "VEN", "value": "Venezuela, Bolivarian Republic of"}, - {"key": "VNM", "value": "Viet Nam"}, - {"key": "VGB", "value": "Virgin Islands (British)"}, - {"key": "VIR", "value": "Virgin Islands (U.S.)"}, - {"key": "WLF", "value": "Wallis and Futuna"}, - {"key": "ESH", "value": "Western Sahara"}, - {"key": "YEM", "value": "Yemen"}, - {"key": "ZMB", "value": "Zambia"}, - {"key": "ZWE", "value": "Zimbabwe"} - ] -} +{"name": "country", + "VSName": "Country", + "VCategory": "System", + "values": [ + {"key": "AFG", "value": "Afghanistan"}, + {"key": "ALA", "value": "Åland Islands"}, + {"key": "ALB", "value": "Albania"}, + {"key": "DZA", "value": "Algeria"}, + {"key": "ASM", "value": "American Samoa"}, + {"key": "AND", "value": "Andorra"}, + {"key": "AGO", "value": "Angola"}, + {"key": "AIA", "value": "Anguilla"}, + {"key": "ATA", "value": "Antarctica"}, + {"key": "ATG", "value": "Antigua and Barbuda"}, + {"key": "ARG", "value": "Argentina"}, + {"key": "ARM", "value": "Armenia"}, + {"key": "ABW", "value": "Aruba"}, + {"key": "AUS", "value": "Australia"}, + {"key": "AUT", "value": "Austria"}, + {"key": "AZE", "value": "Azerbaijan"}, + {"key": "BHS", "value": "Bahamas"}, + {"key": "BHR", "value": "Bahrain"}, + {"key": "BGD", "value": "Bangladesh"}, + {"key": "BRB", "value": "Barbados"}, + {"key": "BLR", "value": "Belarus"}, + {"key": "BEL", "value": "Belgium"}, + {"key": "BLZ", "value": "Belize"}, + {"key": "BEN", "value": "Benin"}, + {"key": "BMU", "value": "Bermuda"}, + {"key": "BTN", "value": "Bhutan"}, + {"key": "BOL", "value": "Bolivia, Plurinational State of"}, + {"key": "BES", "value": "Bonaire, Sint Eustatius and Saba"}, + {"key": "BIH", "value": "Bosnia and Herzegovina"}, + {"key": "BWA", "value": "Botswana"}, + {"key": "BVT", "value": "Bouvet Island"}, + {"key": "BRA", "value": "Brazil"}, + {"key": "IOT", "value": "British Indian Ocean Territory"}, + {"key": "BRN", "value": "Brunei Darussalam"}, + {"key": "BGR", "value": "Bulgaria"}, + {"key": "BFA", "value": "Burkina Faso"}, + {"key": "BDI", "value": "Burundi"}, + {"key": "CPV", "value": "Cabo Verde"}, + {"key": "KHM", "value": "Cambodia"}, + {"key": "CMR", "value": "Cameroon"}, + {"key": "CAN", "value": "Canada"}, + {"key": "CYM", "value": "Cayman Islands"}, + {"key": "CAF", "value": "Central African Republic"}, + {"key": "TCD", "value": "Chad"}, + {"key": "CHL", "value": "Chile"}, + {"key": "CHN", "value": "China"}, + {"key": "CXR", "value": "Christmas Island"}, + {"key": "CCK", "value": "Cocos (Keeling) Islands"}, + {"key": "COL", "value": "Colombia"}, + {"key": "COM", "value": "Comoros"}, + {"key": "COG", "value": "Congo"}, + {"key": "COD", "value": "Congo, Democratic Republic of the"}, + {"key": "COK", "value": "Cook Islands"}, + {"key": "CRI", "value": "Costa Rica"}, + {"key": "CIV", "value": "Côte d'Ivoire"}, + {"key": "HRV", "value": "Croatia"}, + {"key": "CUB", "value": "Cuba"}, + {"key": "CUW", "value": "Curaçao"}, + {"key": "CYP", "value": "Cyprus"}, + {"key": "CZE", "value": "Czechia"}, + {"key": "DNK", "value": "Denmark"}, + {"key": "DJI", "value": "Djibouti"}, + {"key": "DMA", "value": "Dominica"}, + {"key": "DOM", "value": "Dominican Republic"}, + {"key": "ECU", "value": "Ecuador"}, + {"key": "EGY", "value": "Egypt"}, + {"key": "SLV", "value": "El Salvador"}, + {"key": "GNQ", "value": "Equatorial Guinea"}, + {"key": "ERI", "value": "Eritrea"}, + {"key": "EST", "value": "Estonia"}, + {"key": "SWZ", "value": "Eswatini"}, + {"key": "ETH", "value": "Ethiopia"}, + {"key": "FLK", "value": "Falkland Islands (Malvinas)"}, + {"key": "FRO", "value": "Faroe Islands"}, + {"key": "FJI", "value": "Fiji"}, + {"key": "FIN", "value": "Finland"}, + {"key": "FRA", "value": "France"}, + {"key": "GUF", "value": "French Guiana"}, + {"key": "PYF", "value": "French Polynesia"}, + {"key": "ATF", "value": "French Southern Territories"}, + {"key": "GAB", "value": "Gabon"}, + {"key": "GMB", "value": "Gambia"}, + {"key": "GEO", "value": "Georgia"}, + {"key": "DEU", "value": "Germany"}, + {"key": "GHA", "value": "Ghana"}, + {"key": "GIB", "value": "Gibraltar"}, + {"key": "GRC", "value": "Greece"}, + {"key": "GRL", "value": "Greenland"}, + {"key": "GRD", "value": "Grenada"}, + {"key": "GLP", "value": "Guadeloupe"}, + {"key": "GUM", "value": "Guam"}, + {"key": "GTM", "value": "Guatemala"}, + {"key": "GGY", "value": "Guernsey"}, + {"key": "GIN", "value": "Guinea"}, + {"key": "GNB", "value": "Guinea-Bissau"}, + {"key": "GUY", "value": "Guyana"}, + {"key": "HTI", "value": "Haiti"}, + {"key": "HMD", "value": "Heard Island and McDonald Islands"}, + {"key": "VAT", "value": "Holy See"}, + {"key": "HND", "value": "Honduras"}, + {"key": "HKG", "value": "Hong Kong"}, + {"key": "HUN", "value": "Hungary"}, + {"key": "ISL", "value": "Iceland"}, + {"key": "IND", "value": "India"}, + {"key": "IDN", "value": "Indonesia"}, + {"key": "IRN", "value": "Iran, Islamic Republic of"}, + {"key": "IRQ", "value": "Iraq"}, + {"key": "IRL", "value": "Ireland"}, + {"key": "IMN", "value": "Isle of Man"}, + {"key": "ISR", "value": "Israel"}, + {"key": "ITA", "value": "Italy"}, + {"key": "JAM", "value": "Jamaica"}, + {"key": "JPN", "value": "Japan"}, + {"key": "JEY", "value": "Jersey"}, + {"key": "JOR", "value": "Jordan"}, + {"key": "KAZ", "value": "Kazakhstan"}, + {"key": "KEN", "value": "Kenya"}, + {"key": "KIR", "value": "Kiribati"}, + {"key": "PRK", "value": "Korea, Democratic People's Republic of"}, + {"key": "KOR", "value": "Korea, Republic of"}, + {"key": "KWT", "value": "Kuwait"}, + {"key": "KGZ", "value": "Kyrgyzstan"}, + {"key": "LAO", "value": "Lao People's Democratic Republic"}, + {"key": "LVA", "value": "Latvia"}, + {"key": "LBN", "value": "Lebanon"}, + {"key": "LSO", "value": "Lesotho"}, + {"key": "LBR", "value": "Liberia"}, + {"key": "LBY", "value": "Libya"}, + {"key": "LIE", "value": "Liechtenstein"}, + {"key": "LTU", "value": "Lithuania"}, + {"key": "LUX", "value": "Luxembourg"}, + {"key": "MAC", "value": "Macao"}, + {"key": "MDG", "value": "Madagascar"}, + {"key": "MWI", "value": "Malawi"}, + {"key": "MYS", "value": "Malaysia"}, + {"key": "MDV", "value": "Maldives"}, + {"key": "MLI", "value": "Mali"}, + {"key": "MLT", "value": "Malta"}, + {"key": "MHL", "value": "Marshall Islands"}, + {"key": "MTQ", "value": "Martinique"}, + {"key": "MRT", "value": "Mauritania"}, + {"key": "MUS", "value": "Mauritius"}, + {"key": "MYT", "value": "Mayotte"}, + {"key": "MEX", "value": "Mexico"}, + {"key": "FSM", "value": "Micronesia, Federated States of"}, + {"key": "MDA", "value": "Moldova, Republic of"}, + {"key": "MCO", "value": "Monaco"}, + {"key": "MNG", "value": "Mongolia"}, + {"key": "MNE", "value": "Montenegro"}, + {"key": "MSR", "value": "Montserrat"}, + {"key": "MAR", "value": "Morocco"}, + {"key": "MOZ", "value": "Mozambique"}, + {"key": "MMR", "value": "Myanmar"}, + {"key": "NAM", "value": "Namibia"}, + {"key": "NRU", "value": "Nauru"}, + {"key": "NPL", "value": "Nepal"}, + {"key": "NLD", "value": "Netherlands, Kingdom of the"}, + {"key": "NCL", "value": "New Caledonia"}, + {"key": "NZL", "value": "New Zealand"}, + {"key": "NIC", "value": "Nicaragua"}, + {"key": "NER", "value": "Niger"}, + {"key": "NGA", "value": "Nigeria"}, + {"key": "NIU", "value": "Niue"}, + {"key": "NFK", "value": "Norfolk Island"}, + {"key": "MKD", "value": "North Macedonia"}, + {"key": "MNP", "value": "Northern Mariana Islands"}, + {"key": "NOR", "value": "Norway"}, + {"key": "OMN", "value": "Oman"}, + {"key": "PAK", "value": "Pakistan"}, + {"key": "PLW", "value": "Palau"}, + {"key": "PSE", "value": "Palestine, State of"}, + {"key": "PAN", "value": "Panama"}, + {"key": "PNG", "value": "Papua New Guinea"}, + {"key": "PRY", "value": "Paraguay"}, + {"key": "PER", "value": "Peru"}, + {"key": "PHL", "value": "Philippines"}, + {"key": "PCN", "value": "Pitcairn"}, + {"key": "POL", "value": "Poland"}, + {"key": "PRT", "value": "Portugal"}, + {"key": "PRI", "value": "Puerto Rico"}, + {"key": "QAT", "value": "Qatar"}, + {"key": "REU", "value": "Réunion"}, + {"key": "ROU", "value": "Romania"}, + {"key": "RUS", "value": "Russian Federation"}, + {"key": "RWA", "value": "Rwanda"}, + {"key": "BLM", "value": "Saint Barthélemy"}, + {"key": "SHN", "value": "Saint Helena, Ascension and Tristan da Cunha"}, + {"key": "KNA", "value": "Saint Kitts and Nevis"}, + {"key": "LCA", "value": "Saint Lucia"}, + {"key": "MAF", "value": "Saint Martin (French part)"}, + {"key": "SPM", "value": "Saint Pierre and Miquelon"}, + {"key": "VCT", "value": "Saint Vincent and the Grenadines"}, + {"key": "WSM", "value": "Samoa"}, + {"key": "SMR", "value": "San Marino"}, + {"key": "STP", "value": "Sao Tome and Principe"}, + {"key": "SAU", "value": "Saudi Arabia"}, + {"key": "SEN", "value": "Senegal"}, + {"key": "SRB", "value": "Serbia"}, + {"key": "SYC", "value": "Seychelles"}, + {"key": "SLE", "value": "Sierra Leone"}, + {"key": "SGP", "value": "Singapore"}, + {"key": "SXM", "value": "Sint Maarten (Dutch part)"}, + {"key": "SVK", "value": "Slovakia"}, + {"key": "SVN", "value": "Slovenia"}, + {"key": "SLB", "value": "Solomon Islands"}, + {"key": "SOM", "value": "Somalia"}, + {"key": "ZAF", "value": "South Africa"}, + {"key": "SGS", "value": "South Georgia and the South Sandwich Islands"}, + {"key": "SSD", "value": "South Sudan"}, + {"key": "ESP", "value": "Spain"}, + {"key": "LKA", "value": "Sri Lanka"}, + {"key": "SDN", "value": "Sudan"}, + {"key": "SUR", "value": "Suriname"}, + {"key": "SJM", "value": "Svalbard and Jan Mayen"}, + {"key": "SWE", "value": "Sweden"}, + {"key": "CHE", "value": "Switzerland"}, + {"key": "SYR", "value": "Syrian Arab Republic"}, + {"key": "TWN", "value": "Taiwan, Province of China"}, + {"key": "TJK", "value": "Tajikistan"}, + {"key": "TZA", "value": "Tanzania, United Republic of"}, + {"key": "THA", "value": "Thailand"}, + {"key": "TLS", "value": "Timor-Leste"}, + {"key": "TGO", "value": "Togo"}, + {"key": "TKL", "value": "Tokelau"}, + {"key": "TON", "value": "Tonga"}, + {"key": "TTO", "value": "Trinidad and Tobago"}, + {"key": "TUN", "value": "Tunisia"}, + {"key": "TUR", "value": "Türkiye"}, + {"key": "TKM", "value": "Turkmenistan"}, + {"key": "TCA", "value": "Turks and Caicos Islands"}, + {"key": "TUV", "value": "Tuvalu"}, + {"key": "UGA", "value": "Uganda"}, + {"key": "UKR", "value": "Ukraine"}, + {"key": "ARE", "value": "United Arab Emirates"}, + {"key": "GBR", "value": "United Kingdom of Great Britain and Northern Ireland"}, + {"key": "USA", "value": "United States of America"}, + {"key": "UMI", "value": "United States Minor Outlying Islands"}, + {"key": "URY", "value": "Uruguay"}, + {"key": "UZB", "value": "Uzbekistan"}, + {"key": "VUT", "value": "Vanuatu"}, + {"key": "VEN", "value": "Venezuela, Bolivarian Republic of"}, + {"key": "VNM", "value": "Viet Nam"}, + {"key": "VGB", "value": "Virgin Islands (British)"}, + {"key": "VIR", "value": "Virgin Islands (U.S.)"}, + {"key": "WLF", "value": "Wallis and Futuna"}, + {"key": "ESH", "value": "Western Sahara"}, + {"key": "YEM", "value": "Yemen"}, + {"key": "ZMB", "value": "Zambia"}, + {"key": "ZWE", "value": "Zimbabwe"} + ] +} diff --git a/app/Libraries/Data/ethnic.json b/app/Libraries/Data/ethnic.json index c417cb6..2301943 100755 --- a/app/Libraries/Data/ethnic.json +++ b/app/Libraries/Data/ethnic.json @@ -1,14 +1,14 @@ -{"name": "ethnic", - "VSName": "Ethnic", - "VCategory": "System", - "values": [ - {"key": "PPMLN", "value": "Papua Melanezoid"}, - {"key": "NGRID", "value": "Negroid"}, - {"key": "WDOID", "value": "Weddoid"}, - {"key": "MMPM", "value": "Melayu Mongoloid_Proto Melayu"}, - {"key": "MMDM", "value": "Melayu Mongoloid_Deutro Melayu"}, - {"key": "TNGHA", "value": "Tionghoa"}, - {"key": "INDIA", "value": "India"}, - {"key": "ARAB", "value": "Arab"} - ] -} +{"name": "ethnic", + "VSName": "Ethnic", + "VCategory": "System", + "values": [ + {"key": "PPMLN", "value": "Papua Melanezoid"}, + {"key": "NGRID", "value": "Negroid"}, + {"key": "WDOID", "value": "Weddoid"}, + {"key": "MMPM", "value": "Melayu Mongoloid_Proto Melayu"}, + {"key": "MMDM", "value": "Melayu Mongoloid_Deutro Melayu"}, + {"key": "TNGHA", "value": "Tionghoa"}, + {"key": "INDIA", "value": "India"}, + {"key": "ARAB", "value": "Arab"} + ] +} diff --git a/app/Libraries/Data/event_id.json b/app/Libraries/Data/event_id.json index a6b7f10..013d91c 100755 --- a/app/Libraries/Data/event_id.json +++ b/app/Libraries/Data/event_id.json @@ -1,79 +1,79 @@ -{ - "name": "event_id", - "VSName": "Audit Event ID", - "VCategory": "System", - "values": [ - {"key": "PATIENT_REGISTERED", "value": "Patient registered"}, - {"key": "PATIENT_DEMOGRAPHICS_UPDATED", "value": "Patient demographics updated"}, - {"key": "PATIENT_MERGED", "value": "Patient merged"}, - {"key": "PATIENT_UNMERGED", "value": "Patient unmerged"}, - {"key": "PATIENT_IDENTIFIER_UPDATED", "value": "Patient identifier updated"}, - {"key": "PATIENT_CONSENT_UPDATED", "value": "Patient consent updated"}, - {"key": "PATIENT_INSURANCE_UPDATED", "value": "Patient insurance updated"}, - {"key": "PATIENT_DELETED", "value": "Patient deleted"}, - {"key": "VISIT_ADMITTED", "value": "Visit admitted"}, - {"key": "VISIT_TRANSFERRED", "value": "Visit transferred"}, - {"key": "VISIT_DISCHARGED", "value": "Visit discharged"}, - {"key": "VISIT_STATUS_UPDATED", "value": "Visit status updated"}, - {"key": "ORDER_CREATED", "value": "Order created"}, - {"key": "ORDER_CANCELLED", "value": "Order cancelled"}, - {"key": "ORDER_REOPENED", "value": "Order reopened"}, - {"key": "ORDER_TEST_ADDED", "value": "Order test added"}, - {"key": "ORDER_TEST_REMOVED", "value": "Order test removed"}, - {"key": "SPECIMEN_COLLECTED", "value": "Specimen collected"}, - {"key": "SPECIMEN_RECEIVED", "value": "Specimen received"}, - {"key": "SPECIMEN_REJECTED", "value": "Specimen rejected"}, - {"key": "SPECIMEN_ALIQUOTED", "value": "Specimen aliquoted"}, - {"key": "SPECIMEN_DISPOSED", "value": "Specimen disposed"}, - {"key": "RESULT_ENTERED", "value": "Result entered"}, - {"key": "RESULT_UPDATED", "value": "Result updated"}, - {"key": "RESULT_VERIFIED", "value": "Result verified"}, - {"key": "RESULT_AMENDED", "value": "Result amended"}, - {"key": "RESULT_RELEASED", "value": "Result released"}, - {"key": "RESULT_RETRACTED", "value": "Result retracted"}, - {"key": "RESULT_CORRECTED", "value": "Result corrected"}, - {"key": "QC_RECORDED", "value": "QC recorded"}, - {"key": "QC_FAILED", "value": "QC failed"}, - {"key": "QC_OVERRIDE_APPLIED", "value": "QC override applied"}, - {"key": "VALUESET_ITEM_CREATED", "value": "Value set item created"}, - {"key": "VALUESET_ITEM_UPDATED", "value": "Value set item updated"}, - {"key": "VALUESET_ITEM_RETIRED", "value": "Value set item retired"}, - {"key": "TEST_DEFINITION_UPDATED", "value": "Test definition updated"}, - {"key": "REFERENCE_RANGE_UPDATED", "value": "Reference range updated"}, - {"key": "TEST_PANEL_MEMBERSHIP_UPDATED", "value": "Test panel membership updated"}, - {"key": "ANALYZER_CONFIG_UPDATED", "value": "Analyzer config updated"}, - {"key": "INTEGRATION_CONFIG_UPDATED", "value": "Integration config updated"}, - {"key": "CODING_SYSTEM_UPDATED", "value": "Coding system updated"}, - {"key": "USER_CREATED", "value": "User created"}, - {"key": "USER_DISABLED", "value": "User disabled"}, - {"key": "USER_PASSWORD_RESET", "value": "User password reset"}, - {"key": "USER_ROLE_CHANGED", "value": "User role changed"}, - {"key": "USER_PERMISSION_CHANGED", "value": "User permission changed"}, - {"key": "SITE_CREATED", "value": "Site created"}, - {"key": "SITE_UPDATED", "value": "Site updated"}, - {"key": "WORKSTATION_UPDATED", "value": "Workstation updated"}, - {"key": "AUTH_LOGIN_SUCCESS", "value": "Auth login success"}, - {"key": "AUTH_LOGOUT_SUCCESS", "value": "Auth logout success"}, - {"key": "AUTH_LOGIN_FAILED", "value": "Auth login failed"}, - {"key": "AUTH_LOCKOUT_TRIGGERED", "value": "Auth lockout triggered"}, - {"key": "TOKEN_ISSUED", "value": "Token issued"}, - {"key": "TOKEN_REFRESHED", "value": "Token refreshed"}, - {"key": "TOKEN_REVOKED", "value": "Token revoked"}, - {"key": "AUTHORIZATION_FAILED", "value": "Authorization failed"}, - {"key": "IMPORT_JOB_STARTED", "value": "Import job started"}, - {"key": "IMPORT_JOB_FINISHED", "value": "Import job finished"}, - {"key": "EXPORT_JOB_STARTED", "value": "Export job started"}, - {"key": "EXPORT_JOB_FINISHED", "value": "Export job finished"}, - {"key": "JOB_STARTED", "value": "Job started"}, - {"key": "JOB_FINISHED", "value": "Job finished"}, - {"key": "INTEGRATION_SYNC_STARTED", "value": "Integration sync started"}, - {"key": "INTEGRATION_SYNC_FINISHED", "value": "Integration sync finished"}, - {"key": "AUDIT_WRITE_FAILED", "value": "Audit write failed"}, - {"key": "AUDIT_ARCHIVE_EXECUTED", "value": "Audit archive executed"}, - {"key": "AUDIT_PURGE_EXECUTED", "value": "Audit purge executed"}, - {"key": "AUDIT_CHECKSUM_CREATED", "value": "Audit checksum created"}, - {"key": "AUDIT_CHECKSUM_FAILED", "value": "Audit checksum failed"}, - {"key": "LEGAL_HOLD_APPLIED", "value": "Legal hold applied"}, - {"key": "LEGAL_HOLD_RELEASED", "value": "Legal hold released"} - ] -} +{ + "name": "event_id", + "VSName": "Audit Event ID", + "VCategory": "System", + "values": [ + {"key": "PATIENT_REGISTERED", "value": "Patient registered"}, + {"key": "PATIENT_DEMOGRAPHICS_UPDATED", "value": "Patient demographics updated"}, + {"key": "PATIENT_MERGED", "value": "Patient merged"}, + {"key": "PATIENT_UNMERGED", "value": "Patient unmerged"}, + {"key": "PATIENT_IDENTIFIER_UPDATED", "value": "Patient identifier updated"}, + {"key": "PATIENT_CONSENT_UPDATED", "value": "Patient consent updated"}, + {"key": "PATIENT_INSURANCE_UPDATED", "value": "Patient insurance updated"}, + {"key": "PATIENT_DELETED", "value": "Patient deleted"}, + {"key": "VISIT_ADMITTED", "value": "Visit admitted"}, + {"key": "VISIT_TRANSFERRED", "value": "Visit transferred"}, + {"key": "VISIT_DISCHARGED", "value": "Visit discharged"}, + {"key": "VISIT_STATUS_UPDATED", "value": "Visit status updated"}, + {"key": "ORDER_CREATED", "value": "Order created"}, + {"key": "ORDER_CANCELLED", "value": "Order cancelled"}, + {"key": "ORDER_REOPENED", "value": "Order reopened"}, + {"key": "ORDER_TEST_ADDED", "value": "Order test added"}, + {"key": "ORDER_TEST_REMOVED", "value": "Order test removed"}, + {"key": "SPECIMEN_COLLECTED", "value": "Specimen collected"}, + {"key": "SPECIMEN_RECEIVED", "value": "Specimen received"}, + {"key": "SPECIMEN_REJECTED", "value": "Specimen rejected"}, + {"key": "SPECIMEN_ALIQUOTED", "value": "Specimen aliquoted"}, + {"key": "SPECIMEN_DISPOSED", "value": "Specimen disposed"}, + {"key": "RESULT_ENTERED", "value": "Result entered"}, + {"key": "RESULT_UPDATED", "value": "Result updated"}, + {"key": "RESULT_VERIFIED", "value": "Result verified"}, + {"key": "RESULT_AMENDED", "value": "Result amended"}, + {"key": "RESULT_RELEASED", "value": "Result released"}, + {"key": "RESULT_RETRACTED", "value": "Result retracted"}, + {"key": "RESULT_CORRECTED", "value": "Result corrected"}, + {"key": "QC_RECORDED", "value": "QC recorded"}, + {"key": "QC_FAILED", "value": "QC failed"}, + {"key": "QC_OVERRIDE_APPLIED", "value": "QC override applied"}, + {"key": "VALUESET_ITEM_CREATED", "value": "Value set item created"}, + {"key": "VALUESET_ITEM_UPDATED", "value": "Value set item updated"}, + {"key": "VALUESET_ITEM_RETIRED", "value": "Value set item retired"}, + {"key": "TEST_DEFINITION_UPDATED", "value": "Test definition updated"}, + {"key": "REFERENCE_RANGE_UPDATED", "value": "Reference range updated"}, + {"key": "TEST_PANEL_MEMBERSHIP_UPDATED", "value": "Test panel membership updated"}, + {"key": "ANALYZER_CONFIG_UPDATED", "value": "Analyzer config updated"}, + {"key": "INTEGRATION_CONFIG_UPDATED", "value": "Integration config updated"}, + {"key": "CODING_SYSTEM_UPDATED", "value": "Coding system updated"}, + {"key": "USER_CREATED", "value": "User created"}, + {"key": "USER_DISABLED", "value": "User disabled"}, + {"key": "USER_PASSWORD_RESET", "value": "User password reset"}, + {"key": "USER_ROLE_CHANGED", "value": "User role changed"}, + {"key": "USER_PERMISSION_CHANGED", "value": "User permission changed"}, + {"key": "SITE_CREATED", "value": "Site created"}, + {"key": "SITE_UPDATED", "value": "Site updated"}, + {"key": "WORKSTATION_UPDATED", "value": "Workstation updated"}, + {"key": "AUTH_LOGIN_SUCCESS", "value": "Auth login success"}, + {"key": "AUTH_LOGOUT_SUCCESS", "value": "Auth logout success"}, + {"key": "AUTH_LOGIN_FAILED", "value": "Auth login failed"}, + {"key": "AUTH_LOCKOUT_TRIGGERED", "value": "Auth lockout triggered"}, + {"key": "TOKEN_ISSUED", "value": "Token issued"}, + {"key": "TOKEN_REFRESHED", "value": "Token refreshed"}, + {"key": "TOKEN_REVOKED", "value": "Token revoked"}, + {"key": "AUTHORIZATION_FAILED", "value": "Authorization failed"}, + {"key": "IMPORT_JOB_STARTED", "value": "Import job started"}, + {"key": "IMPORT_JOB_FINISHED", "value": "Import job finished"}, + {"key": "EXPORT_JOB_STARTED", "value": "Export job started"}, + {"key": "EXPORT_JOB_FINISHED", "value": "Export job finished"}, + {"key": "JOB_STARTED", "value": "Job started"}, + {"key": "JOB_FINISHED", "value": "Job finished"}, + {"key": "INTEGRATION_SYNC_STARTED", "value": "Integration sync started"}, + {"key": "INTEGRATION_SYNC_FINISHED", "value": "Integration sync finished"}, + {"key": "AUDIT_WRITE_FAILED", "value": "Audit write failed"}, + {"key": "AUDIT_ARCHIVE_EXECUTED", "value": "Audit archive executed"}, + {"key": "AUDIT_PURGE_EXECUTED", "value": "Audit purge executed"}, + {"key": "AUDIT_CHECKSUM_CREATED", "value": "Audit checksum created"}, + {"key": "AUDIT_CHECKSUM_FAILED", "value": "Audit checksum failed"}, + {"key": "LEGAL_HOLD_APPLIED", "value": "Legal hold applied"}, + {"key": "LEGAL_HOLD_RELEASED", "value": "Legal hold released"} + ] +} diff --git a/app/Libraries/Data/fasting_status.json b/app/Libraries/Data/fasting_status.json index 03e6314..40fd664 100755 --- a/app/Libraries/Data/fasting_status.json +++ b/app/Libraries/Data/fasting_status.json @@ -1,9 +1,9 @@ -{"name": "fasting_status", - "VSName": "Fasting Status", - "VCategory": "System", - "values": [ - {"key": "F", "value": "Fasting"}, - {"key": "NF", "value": "Not Fasting"}, - {"key": "NG", "value": "Not Given"} - ] -} +{"name": "fasting_status", + "VSName": "Fasting Status", + "VCategory": "System", + "values": [ + {"key": "F", "value": "Fasting"}, + {"key": "NF", "value": "Not Fasting"}, + {"key": "NG", "value": "Not Given"} + ] +} diff --git a/app/Libraries/Data/formula_language.json b/app/Libraries/Data/formula_language.json index 9bc3513..d33a398 100755 --- a/app/Libraries/Data/formula_language.json +++ b/app/Libraries/Data/formula_language.json @@ -1,10 +1,10 @@ -{"name": "formula_language", - "VSName": "Formula Language", - "VCategory": "System", - "values": [ - {"key": "Phyton", "value": "Phyton"}, - {"key": "CQL", "value": "Clinical Quality Language"}, - {"key": "FHIRP", "value": "FHIRPath"}, - {"key": "SQL", "value": "SQL"} - ] -} +{"name": "formula_language", + "VSName": "Formula Language", + "VCategory": "System", + "values": [ + {"key": "Phyton", "value": "Phyton"}, + {"key": "CQL", "value": "Clinical Quality Language"}, + {"key": "FHIRP", "value": "FHIRPath"}, + {"key": "SQL", "value": "SQL"} + ] +} diff --git a/app/Libraries/Data/identifier_type.json b/app/Libraries/Data/identifier_type.json index a4cd258..bbb9db3 100755 --- a/app/Libraries/Data/identifier_type.json +++ b/app/Libraries/Data/identifier_type.json @@ -1,11 +1,11 @@ -{"name": "identifier_type", - "VSName": "Identifier Type", - "VCategory": "System", - "values": [ - {"key": "KTP", "value": "Kartu Tanda Penduduk"}, - {"key": "PASS", "value": "Passport"}, - {"key": "SSN", "value": "Social Security Number"}, - {"key": "SIM", "value": "Surat Izin Mengemudi"}, - {"key": "KTAS", "value": "Kartu Izin Tinggal Terbatas"} - ] -} +{"name": "identifier_type", + "VSName": "Identifier Type", + "VCategory": "System", + "values": [ + {"key": "KTP", "value": "Kartu Tanda Penduduk"}, + {"key": "PASS", "value": "Passport"}, + {"key": "SSN", "value": "Social Security Number"}, + {"key": "SIM", "value": "Surat Izin Mengemudi"}, + {"key": "KTAS", "value": "Kartu Izin Tinggal Terbatas"} + ] +} diff --git a/app/Libraries/Data/location_type.json b/app/Libraries/Data/location_type.json index 7c38fe4..34f45c8 100755 --- a/app/Libraries/Data/location_type.json +++ b/app/Libraries/Data/location_type.json @@ -1,14 +1,14 @@ -{"name": "location_type", - "VSName": "Location Type", - "VCategory": "System", - "values": [ - {"key": "FCLT", "value": "Facility"}, - {"key": "BLDG", "value": "Building"}, - {"key": "FLOR", "value": "Floor"}, - {"key": "POC", "value": "Point of Care"}, - {"key": "ROOM", "value": "Room"}, - {"key": "BED", "value": "Bed"}, - {"key": "MOBL", "value": "Mobile"}, - {"key": "REMT", "value": "Remote"} - ] -} +{"name": "location_type", + "VSName": "Location Type", + "VCategory": "System", + "values": [ + {"key": "FCLT", "value": "Facility"}, + {"key": "BLDG", "value": "Building"}, + {"key": "FLOR", "value": "Floor"}, + {"key": "POC", "value": "Point of Care"}, + {"key": "ROOM", "value": "Room"}, + {"key": "BED", "value": "Bed"}, + {"key": "MOBL", "value": "Mobile"}, + {"key": "REMT", "value": "Remote"} + ] +} diff --git a/app/Libraries/Data/marital_status.json b/app/Libraries/Data/marital_status.json index ff6f2cc..f175c52 100755 --- a/app/Libraries/Data/marital_status.json +++ b/app/Libraries/Data/marital_status.json @@ -1,14 +1,14 @@ -{"name": "marital_status", - "VSName": "Marital Status", - "VCategory": "System", - "values": [ - {"key": "A", "value": "Separated"}, - {"key": "D", "value": "Divorced"}, - {"key": "M", "value": "Married"}, - {"key": "S", "value": "Single"}, - {"key": "W", "value": "Widowed"}, - {"key": "B", "value": "Unmarried"}, - {"key": "U", "value": "Unknown"}, - {"key": "O", "value": "Other"} - ] -} +{"name": "marital_status", + "VSName": "Marital Status", + "VCategory": "System", + "values": [ + {"key": "A", "value": "Separated"}, + {"key": "D", "value": "Divorced"}, + {"key": "M", "value": "Married"}, + {"key": "S", "value": "Single"}, + {"key": "W", "value": "Widowed"}, + {"key": "B", "value": "Unmarried"}, + {"key": "U", "value": "Unknown"}, + {"key": "O", "value": "Other"} + ] +} diff --git a/app/Libraries/Data/math_sign.json b/app/Libraries/Data/math_sign.json index 9e5f1c1..bacb341 100755 --- a/app/Libraries/Data/math_sign.json +++ b/app/Libraries/Data/math_sign.json @@ -1,11 +1,11 @@ -{"name": "math_sign", - "VSName": "Math Sign", - "VCategory": "System", - "values": [ - {"key": "=", "value": "Equal"}, - {"key": "<", "value": "Less than"}, - {"key": ">", "value": "Greater than"}, - {"key": "<=", "value": "Less than or equal to"}, - {"key": ">=", "value": "Greater than or equal to"} - ] -} +{"name": "math_sign", + "VSName": "Math Sign", + "VCategory": "System", + "values": [ + {"key": "=", "value": "Equal"}, + {"key": "<", "value": "Less than"}, + {"key": ">", "value": "Greater than"}, + {"key": "<=", "value": "Less than or equal to"}, + {"key": ">=", "value": "Greater than or equal to"} + ] +} diff --git a/app/Libraries/Data/numeric_ref_type.json b/app/Libraries/Data/numeric_ref_type.json index fb5afd3..288e770 100755 --- a/app/Libraries/Data/numeric_ref_type.json +++ b/app/Libraries/Data/numeric_ref_type.json @@ -1,8 +1,8 @@ -{"name": "numeric_ref_type", - "VSName": "Numeric Reference Type", - "VCategory": "System", - "values": [ - {"key": "RANGE", "value": "Range"}, - {"key": "THOLD", "value": "Threshold"} - ] -} +{"name": "numeric_ref_type", + "VSName": "Numeric Reference Type", + "VCategory": "System", + "values": [ + {"key": "RANGE", "value": "Range"}, + {"key": "THOLD", "value": "Threshold"} + ] +} diff --git a/app/Libraries/Data/race.json b/app/Libraries/Data/race.json index 5f29061..48d58a5 100755 --- a/app/Libraries/Data/race.json +++ b/app/Libraries/Data/race.json @@ -1,37 +1,37 @@ -{"name": "race", - "VSName": "Race (Ethnicity)", - "VCategory": "System", - "values": [ - {"key": "JAWA", "value": "Jawa"}, - {"key": "SUNDA", "value": "Sunda"}, - {"key": "BATAK", "value": "Batak"}, - {"key": "SULOR", "value": "Suku asal Sulawesi lainnya"}, - {"key": "MDRA", "value": "Madura"}, - {"key": "BTWI", "value": "Betawi"}, - {"key": "MNG", "value": "Minangkabau"}, - {"key": "BUGIS", "value": "Bugis"}, - {"key": "MLYU", "value": "Melayu"}, - {"key": "SUMSL", "value": "Suku asal Sumatera Selatan"}, - {"key": "BTNOR", "value": "Suku asal Banten"}, - {"key": "NTTOR", "value": "Suku asal Nusa Tenggara Timur"}, - {"key": "BNJAR", "value": "Banjar"}, - {"key": "ACEH", "value": "Aceh"}, - {"key": "BALI", "value": "Bali"}, - {"key": "SASAK", "value": "Sasak"}, - {"key": "DAYAK", "value": "Dayak"}, - {"key": "TNGHA", "value": "Tionghoa"}, - {"key": "PPAOR", "value": "Suku asal Papua"}, - {"key": "MKSSR", "value": "Makassar"}, - {"key": "SUMOR", "value": "Suku asal Sumatera lainnya"}, - {"key": "MLKOR", "value": "Suku asal Maluku"}, - {"key": "KLMOR", "value": "Suku asal Kalimantan lainnya"}, - {"key": "CRBON", "value": "Cirebon"}, - {"key": "JBIOR", "value": "Suku asal Jambi"}, - {"key": "LPGOR", "value": "Suku Lampung"}, - {"key": "NTBOR", "value": "Suku asal Nusa Tenggara Barat lainnya"}, - {"key": "GRTLO", "value": "Gorontalo"}, - {"key": "MNHSA", "value": "Minahasa"}, - {"key": "NIAS", "value": "Nias"}, - {"key": "FORGN", "value": "Asing/luar negeri"} - ] -} +{"name": "race", + "VSName": "Race (Ethnicity)", + "VCategory": "System", + "values": [ + {"key": "JAWA", "value": "Jawa"}, + {"key": "SUNDA", "value": "Sunda"}, + {"key": "BATAK", "value": "Batak"}, + {"key": "SULOR", "value": "Suku asal Sulawesi lainnya"}, + {"key": "MDRA", "value": "Madura"}, + {"key": "BTWI", "value": "Betawi"}, + {"key": "MNG", "value": "Minangkabau"}, + {"key": "BUGIS", "value": "Bugis"}, + {"key": "MLYU", "value": "Melayu"}, + {"key": "SUMSL", "value": "Suku asal Sumatera Selatan"}, + {"key": "BTNOR", "value": "Suku asal Banten"}, + {"key": "NTTOR", "value": "Suku asal Nusa Tenggara Timur"}, + {"key": "BNJAR", "value": "Banjar"}, + {"key": "ACEH", "value": "Aceh"}, + {"key": "BALI", "value": "Bali"}, + {"key": "SASAK", "value": "Sasak"}, + {"key": "DAYAK", "value": "Dayak"}, + {"key": "TNGHA", "value": "Tionghoa"}, + {"key": "PPAOR", "value": "Suku asal Papua"}, + {"key": "MKSSR", "value": "Makassar"}, + {"key": "SUMOR", "value": "Suku asal Sumatera lainnya"}, + {"key": "MLKOR", "value": "Suku asal Maluku"}, + {"key": "KLMOR", "value": "Suku asal Kalimantan lainnya"}, + {"key": "CRBON", "value": "Cirebon"}, + {"key": "JBIOR", "value": "Suku asal Jambi"}, + {"key": "LPGOR", "value": "Suku Lampung"}, + {"key": "NTBOR", "value": "Suku asal Nusa Tenggara Barat lainnya"}, + {"key": "GRTLO", "value": "Gorontalo"}, + {"key": "MNHSA", "value": "Minahasa"}, + {"key": "NIAS", "value": "Nias"}, + {"key": "FORGN", "value": "Asing/luar negeri"} + ] +} diff --git a/app/Libraries/Data/range_type.json b/app/Libraries/Data/range_type.json index 31a7d76..d925a21 100755 --- a/app/Libraries/Data/range_type.json +++ b/app/Libraries/Data/range_type.json @@ -1,10 +1,10 @@ -{"name": "range_type", - "VSName": "Range Type", - "VCategory": "System", - "values": [ - {"key": "REF", "value": "Reference Range"}, - {"key": "CRTC", "value": "Critical Range"}, - {"key": "VAL", "value": "Validation Range"}, - {"key": "RERUN", "value": "Rerun Range"} - ] -} +{"name": "range_type", + "VSName": "Range Type", + "VCategory": "System", + "values": [ + {"key": "REF", "value": "Reference Range"}, + {"key": "CRTC", "value": "Critical Range"}, + {"key": "VAL", "value": "Validation Range"}, + {"key": "RERUN", "value": "Rerun Range"} + ] +} diff --git a/app/Libraries/Data/religion.json b/app/Libraries/Data/religion.json index a16915a..678ff89 100755 --- a/app/Libraries/Data/religion.json +++ b/app/Libraries/Data/religion.json @@ -1,13 +1,13 @@ -{"name": "religion", - "VSName": "Religion", - "VCategory": "System", - "values": [ - {"key": "ISLAM", "value": "Islam"}, - {"key": "KRSTN", "value": "Kristen"}, - {"key": "KTLIK", "value": "Katolik"}, - {"key": "HINDU", "value": "Hindu"}, - {"key": "BUDHA", "value": "Budha"}, - {"key": "KHCU", "value": "Khong Hu Cu"}, - {"key": "OTHER", "value": "Lainnya"} - ] -} +{"name": "religion", + "VSName": "Religion", + "VCategory": "System", + "values": [ + {"key": "ISLAM", "value": "Islam"}, + {"key": "KRSTN", "value": "Kristen"}, + {"key": "KTLIK", "value": "Katolik"}, + {"key": "HINDU", "value": "Hindu"}, + {"key": "BUDHA", "value": "Budha"}, + {"key": "KHCU", "value": "Khong Hu Cu"}, + {"key": "OTHER", "value": "Lainnya"} + ] +} diff --git a/app/Libraries/Data/request_status.json b/app/Libraries/Data/request_status.json index 44646df..9283f79 100755 --- a/app/Libraries/Data/request_status.json +++ b/app/Libraries/Data/request_status.json @@ -1,26 +1,26 @@ -{"name": "request_status", - "VSName": "Request Status", - "VCategory": "System", - "values": [ - {"key": "STC", "value": "To be collected"}, - {"key": "SCFld", "value": "Collection failed"}, - {"key": "SCtd", "value": "Collected"}, - {"key": "STran", "value": "In-transport"}, - {"key": "STFld", "value": "Transport failed"}, - {"key": "SArrv", "value": "Arrived"}, - {"key": "SRejc", "value": "Rejected"}, - {"key": "SRcvd", "value": "Received"}, - {"key": "SPAna", "value": "Pre-analytical"}, - {"key": "SPAF", "value": "Pre-analytical failed"}, - {"key": "STA", "value": "To be analyze"}, - {"key": "SAFld", "value": "Analytical failed"}, - {"key": "SAna", "value": "Analytical"}, - {"key": "STS", "value": "To be stored"}, - {"key": "SSFld", "value": "Store failed"}, - {"key": "SStrd", "value": "Stored"}, - {"key": "SExp", "value": "Expired"}, - {"key": "STD", "value": "To be destroyed"}, - {"key": "SDFld", "value": "Failed to destroy"}, - {"key": "SDstd", "value": "Destroyed"} - ] -} +{"name": "request_status", + "VSName": "Request Status", + "VCategory": "System", + "values": [ + {"key": "STC", "value": "To be collected"}, + {"key": "SCFld", "value": "Collection failed"}, + {"key": "SCtd", "value": "Collected"}, + {"key": "STran", "value": "In-transport"}, + {"key": "STFld", "value": "Transport failed"}, + {"key": "SArrv", "value": "Arrived"}, + {"key": "SRejc", "value": "Rejected"}, + {"key": "SRcvd", "value": "Received"}, + {"key": "SPAna", "value": "Pre-analytical"}, + {"key": "SPAF", "value": "Pre-analytical failed"}, + {"key": "STA", "value": "To be analyze"}, + {"key": "SAFld", "value": "Analytical failed"}, + {"key": "SAna", "value": "Analytical"}, + {"key": "STS", "value": "To be stored"}, + {"key": "SSFld", "value": "Store failed"}, + {"key": "SStrd", "value": "Stored"}, + {"key": "SExp", "value": "Expired"}, + {"key": "STD", "value": "To be destroyed"}, + {"key": "SDFld", "value": "Failed to destroy"}, + {"key": "SDstd", "value": "Destroyed"} + ] +} diff --git a/app/Libraries/Data/result_status.json b/app/Libraries/Data/result_status.json index 299da23..7bf13af 100755 --- a/app/Libraries/Data/result_status.json +++ b/app/Libraries/Data/result_status.json @@ -1,10 +1,10 @@ -{"name": "result_status", - "VSName": "Result Status", - "VCategory": "System", - "values": [ - {"key": "PRELIMINARY", "value": "Preliminary"}, - {"key": "FINAL", "value": "Final"}, - {"key": "CORRECTED", "value": "Corrected"}, - {"key": "CANCELLED", "value": "Cancelled"} - ] -} +{"name": "result_status", + "VSName": "Result Status", + "VCategory": "System", + "values": [ + {"key": "PRELIMINARY", "value": "Preliminary"}, + {"key": "FINAL", "value": "Final"}, + {"key": "CORRECTED", "value": "Corrected"}, + {"key": "CANCELLED", "value": "Cancelled"} + ] +} diff --git a/app/Libraries/Data/result_unit.json b/app/Libraries/Data/result_unit.json index ae45dc5..ffc1e42 100755 --- a/app/Libraries/Data/result_unit.json +++ b/app/Libraries/Data/result_unit.json @@ -1,16 +1,16 @@ -{"name": "result_unit", - "VSName": "Result Unit", - "VCategory": "System", - "values": [ - {"key": "g/dL", "value": "g/dL"}, - {"key": "g/L", "value": "g/L"}, - {"key": "mg/dL", "value": "mg/dL"}, - {"key": "mg/L", "value": "mg/L"}, - {"key": "L/L", "value": "L/L"}, - {"key": "x106/mL", "value": "x106/mL"}, - {"key": "x1012/L", "value": "x1012/L"}, - {"key": "fL", "value": "fL"}, - {"key": "pg", "value": "pg"}, - {"key": "x109/L", "value": "x109/L"} - ] -} +{"name": "result_unit", + "VSName": "Result Unit", + "VCategory": "System", + "values": [ + {"key": "g/dL", "value": "g/dL"}, + {"key": "g/L", "value": "g/L"}, + {"key": "mg/dL", "value": "mg/dL"}, + {"key": "mg/L", "value": "mg/L"}, + {"key": "L/L", "value": "L/L"}, + {"key": "x106/mL", "value": "x106/mL"}, + {"key": "x1012/L", "value": "x1012/L"}, + {"key": "fL", "value": "fL"}, + {"key": "pg", "value": "pg"}, + {"key": "x109/L", "value": "x109/L"} + ] +} diff --git a/app/Libraries/Data/site_class.json b/app/Libraries/Data/site_class.json index ea33086..7245392 100755 --- a/app/Libraries/Data/site_class.json +++ b/app/Libraries/Data/site_class.json @@ -1,12 +1,12 @@ -{"name": "site_class", - "VSName": "Site Class", - "VCategory": "System", - "values": [ - {"key": "A", "value": "Kelas A"}, - {"key": "B", "value": "Kelas B"}, - {"key": "C", "value": "Kelas C"}, - {"key": "D", "value": "Kelas D"}, - {"key": "Utm", "value": "Utama"}, - {"key": "Ptm", "value": "Pratama"} - ] -} +{"name": "site_class", + "VSName": "Site Class", + "VCategory": "System", + "values": [ + {"key": "A", "value": "Kelas A"}, + {"key": "B", "value": "Kelas B"}, + {"key": "C", "value": "Kelas C"}, + {"key": "D", "value": "Kelas D"}, + {"key": "Utm", "value": "Utama"}, + {"key": "Ptm", "value": "Pratama"} + ] +} diff --git a/app/Libraries/Data/site_type.json b/app/Libraries/Data/site_type.json index f094d91..9e98c3e 100755 --- a/app/Libraries/Data/site_type.json +++ b/app/Libraries/Data/site_type.json @@ -1,12 +1,12 @@ -{"name": "site_type", - "VSName": "Site Type", - "VCategory": "System", - "values": [ - {"key": "GH", "value": "Government Hospital"}, - {"key": "PH", "value": "Private Hospital"}, - {"key": "GHL", "value": "Government Hospital Lab"}, - {"key": "PHL", "value": "Private Hospital Lab"}, - {"key": "GL", "value": "Government Lab"}, - {"key": "PL", "value": "Private Lab"} - ] -} +{"name": "site_type", + "VSName": "Site Type", + "VCategory": "System", + "values": [ + {"key": "GH", "value": "Government Hospital"}, + {"key": "PH", "value": "Private Hospital"}, + {"key": "GHL", "value": "Government Hospital Lab"}, + {"key": "PHL", "value": "Private Hospital Lab"}, + {"key": "GL", "value": "Government Lab"}, + {"key": "PL", "value": "Private Lab"} + ] +} diff --git a/app/Libraries/Data/specimen_activity.json b/app/Libraries/Data/specimen_activity.json index 4670c15..9c24b39 100755 --- a/app/Libraries/Data/specimen_activity.json +++ b/app/Libraries/Data/specimen_activity.json @@ -1,13 +1,13 @@ -{"name": "specimen_activity", - "VSName": "Specimen Activity", - "VCategory": "System", - "values": [ - {"key": "SColl", "value": "Collection"}, - {"key": "STran", "value": "Transport"}, - {"key": "SRec", "value": "Reception"}, - {"key": "SPrep", "value": "Preparation"}, - {"key": "SAlqt", "value": "Aliquot"}, - {"key": "SDisp", "value": "Dispatching"}, - {"key": "SDest", "value": "Destruction"} - ] -} +{"name": "specimen_activity", + "VSName": "Specimen Activity", + "VCategory": "System", + "values": [ + {"key": "SColl", "value": "Collection"}, + {"key": "STran", "value": "Transport"}, + {"key": "SRec", "value": "Reception"}, + {"key": "SPrep", "value": "Preparation"}, + {"key": "SAlqt", "value": "Aliquot"}, + {"key": "SDisp", "value": "Dispatching"}, + {"key": "SDest", "value": "Destruction"} + ] +} diff --git a/app/Libraries/Data/specimen_condition.json b/app/Libraries/Data/specimen_condition.json index 9bc4ee0..cd3dff5 100755 --- a/app/Libraries/Data/specimen_condition.json +++ b/app/Libraries/Data/specimen_condition.json @@ -1,17 +1,17 @@ -{"name": "specimen_condition", - "VSName": "Specimen Condition", - "VCategory": "System", - "values": [ - {"key": "HEM", "value": "Hemolyzed"}, - {"key": "ITC", "value": "Icteric"}, - {"key": "LIP", "value": "Lipemic"}, - {"key": "CFU", "value": "Centrifuged"}, - {"key": "ROOM", "value": "Room temperature"}, - {"key": "COOL", "value": "Cool"}, - {"key": "FROZ", "value": "Frozen"}, - {"key": "CLOT", "value": "Clotted"}, - {"key": "AUT", "value": "Autolyzed"}, - {"key": "CON", "value": "Contaminated"}, - {"key": "LIVE", "value": "Live"} - ] -} +{"name": "specimen_condition", + "VSName": "Specimen Condition", + "VCategory": "System", + "values": [ + {"key": "HEM", "value": "Hemolyzed"}, + {"key": "ITC", "value": "Icteric"}, + {"key": "LIP", "value": "Lipemic"}, + {"key": "CFU", "value": "Centrifuged"}, + {"key": "ROOM", "value": "Room temperature"}, + {"key": "COOL", "value": "Cool"}, + {"key": "FROZ", "value": "Frozen"}, + {"key": "CLOT", "value": "Clotted"}, + {"key": "AUT", "value": "Autolyzed"}, + {"key": "CON", "value": "Contaminated"}, + {"key": "LIVE", "value": "Live"} + ] +} diff --git a/app/Libraries/Data/specimen_role.json b/app/Libraries/Data/specimen_role.json index a378b8d..d30e4a7 100755 --- a/app/Libraries/Data/specimen_role.json +++ b/app/Libraries/Data/specimen_role.json @@ -1,15 +1,15 @@ -{"name": "specimen_role", - "VSName": "Specimen Role", - "VCategory": "System", - "values": [ - {"key": "P", "value": "Patient"}, - {"key": "B", "value": "Blind Sample"}, - {"key": "Q", "value": "Control specimen"}, - {"key": "E", "value": "Electronic QC"}, - {"key": "F", "value": "Filler Organization Proficiency"}, - {"key": "O", "value": "Operator Proficiency"}, - {"key": "C", "value": "Calibrator"}, - {"key": "R", "value": "Replicate"}, - {"key": "V", "value": "Verifying Calibrator"} - ] -} +{"name": "specimen_role", + "VSName": "Specimen Role", + "VCategory": "System", + "values": [ + {"key": "P", "value": "Patient"}, + {"key": "B", "value": "Blind Sample"}, + {"key": "Q", "value": "Control specimen"}, + {"key": "E", "value": "Electronic QC"}, + {"key": "F", "value": "Filler Organization Proficiency"}, + {"key": "O", "value": "Operator Proficiency"}, + {"key": "C", "value": "Calibrator"}, + {"key": "R", "value": "Replicate"}, + {"key": "V", "value": "Verifying Calibrator"} + ] +} diff --git a/app/Libraries/Data/specimen_status.json b/app/Libraries/Data/specimen_status.json index fe9d7ea..b31b3e5 100755 --- a/app/Libraries/Data/specimen_status.json +++ b/app/Libraries/Data/specimen_status.json @@ -1,26 +1,26 @@ -{"name": "specimen_status", - "VSName": "Specimen Status", - "VCategory": "System", - "values": [ - {"key": "STC", "value": "To be collected"}, - {"key": "SCFld", "value": "Collection failed"}, - {"key": "SCtd", "value": "Collected"}, - {"key": "STran", "value": "In-transport"}, - {"key": "STFld", "value": "Transport failed"}, - {"key": "SArrv", "value": "Arrived"}, - {"key": "SRejc", "value": "Rejected"}, - {"key": "SRcvd", "value": "Received"}, - {"key": "SPAna", "value": "Pre-analytical"}, - {"key": "SPAF", "value": "Pre-analytical failed"}, - {"key": "STA", "value": "To be analyze"}, - {"key": "SAFld", "value": "Analytical failed"}, - {"key": "SAna", "value": "Analytical"}, - {"key": "STS", "value": "To be stored"}, - {"key": "SSFld", "value": "Store failed"}, - {"key": "SStrd", "value": "Stored"}, - {"key": "SExp", "value": "Expired"}, - {"key": "STD", "value": "To be destroyed"}, - {"key": "SDFld", "value": "Failed to destroy"}, - {"key": "SDstd", "value": "Destroyed"} - ] -} +{"name": "specimen_status", + "VSName": "Specimen Status", + "VCategory": "System", + "values": [ + {"key": "STC", "value": "To be collected"}, + {"key": "SCFld", "value": "Collection failed"}, + {"key": "SCtd", "value": "Collected"}, + {"key": "STran", "value": "In-transport"}, + {"key": "STFld", "value": "Transport failed"}, + {"key": "SArrv", "value": "Arrived"}, + {"key": "SRejc", "value": "Rejected"}, + {"key": "SRcvd", "value": "Received"}, + {"key": "SPAna", "value": "Pre-analytical"}, + {"key": "SPAF", "value": "Pre-analytical failed"}, + {"key": "STA", "value": "To be analyze"}, + {"key": "SAFld", "value": "Analytical failed"}, + {"key": "SAna", "value": "Analytical"}, + {"key": "STS", "value": "To be stored"}, + {"key": "SSFld", "value": "Store failed"}, + {"key": "SStrd", "value": "Stored"}, + {"key": "SExp", "value": "Expired"}, + {"key": "STD", "value": "To be destroyed"}, + {"key": "SDFld", "value": "Failed to destroy"}, + {"key": "SDstd", "value": "Destroyed"} + ] +} diff --git a/app/Libraries/Data/specimen_type.json b/app/Libraries/Data/specimen_type.json index 28f6c66..cb20256 100755 --- a/app/Libraries/Data/specimen_type.json +++ b/app/Libraries/Data/specimen_type.json @@ -1,21 +1,21 @@ -{"name": "specimen_type", - "VSName": "Specimen Type", - "VCategory": "System", - "values": [ - {"key": "BLD", "value": "Whole blood"}, - {"key": "BLDA", "value": "Blood arterial"}, - {"key": "BLDCO", "value": "Cord blood"}, - {"key": "FBLOOD", "value": "Blood, Fetal"}, - {"key": "CSF", "value": "Cerebral spinal fluid"}, - {"key": "WB", "value": "Blood, Whole"}, - {"key": "BBL", "value": "Blood bag"}, - {"key": "SER", "value": "Serum"}, - {"key": "PLAS", "value": "Plasma"}, - {"key": "PLB", "value": "Plasma bag"}, - {"key": "MUCOS", "value": "Mucosa"}, - {"key": "MUCUS", "value": "Mucus"}, - {"key": "UR", "value": "Urine"}, - {"key": "RANDU", "value": "Urine, Random"}, - {"key": "URINM", "value": "Urine, Midstream"} - ] -} +{"name": "specimen_type", + "VSName": "Specimen Type", + "VCategory": "System", + "values": [ + {"key": "BLD", "value": "Whole blood"}, + {"key": "BLDA", "value": "Blood arterial"}, + {"key": "BLDCO", "value": "Cord blood"}, + {"key": "FBLOOD", "value": "Blood, Fetal"}, + {"key": "CSF", "value": "Cerebral spinal fluid"}, + {"key": "WB", "value": "Blood, Whole"}, + {"key": "BBL", "value": "Blood bag"}, + {"key": "SER", "value": "Serum"}, + {"key": "PLAS", "value": "Plasma"}, + {"key": "PLB", "value": "Plasma bag"}, + {"key": "MUCOS", "value": "Mucosa"}, + {"key": "MUCUS", "value": "Mucus"}, + {"key": "UR", "value": "Urine"}, + {"key": "RANDU", "value": "Urine, Random"}, + {"key": "URINM", "value": "Urine, Midstream"} + ] +} diff --git a/app/Libraries/Data/test_activity.json b/app/Libraries/Data/test_activity.json index 0282514..830cbb5 100755 --- a/app/Libraries/Data/test_activity.json +++ b/app/Libraries/Data/test_activity.json @@ -1,11 +1,11 @@ -{"name": "test_activity", - "VSName": "Test Activity", - "VCategory": "System", - "values": [ - {"key": "ORD", "value": "Order"}, - {"key": "ANA", "value": "Analyse"}, - {"key": "VER", "value": "Result Verification/Technical Validation"}, - {"key": "REV", "value": "Clinical Review/Clinical Validation"}, - {"key": "REP", "value": "Reporting"} - ] -} +{"name": "test_activity", + "VSName": "Test Activity", + "VCategory": "System", + "values": [ + {"key": "ORD", "value": "Order"}, + {"key": "ANA", "value": "Analyse"}, + {"key": "VER", "value": "Result Verification/Technical Validation"}, + {"key": "REV", "value": "Clinical Review/Clinical Validation"}, + {"key": "REP", "value": "Reporting"} + ] +} diff --git a/app/Libraries/Data/test_status.json b/app/Libraries/Data/test_status.json index 31d1cdf..cd8616b 100755 --- a/app/Libraries/Data/test_status.json +++ b/app/Libraries/Data/test_status.json @@ -1,10 +1,10 @@ -{"name": "test_status", - "VSName": "Test Status", - "VCategory": "System", - "values": [ - {"key": "PENDING", "value": "Waiting for Results"}, - {"key": "IN_PROCESS", "value": "Analyzing"}, - {"key": "VERIFIED", "value": "Verified & Signed"}, - {"key": "REJECTED", "value": "Sample Rejected"} - ] -} +{"name": "test_status", + "VSName": "Test Status", + "VCategory": "System", + "values": [ + {"key": "PENDING", "value": "Waiting for Results"}, + {"key": "IN_PROCESS", "value": "Analyzing"}, + {"key": "VERIFIED", "value": "Verified & Signed"}, + {"key": "REJECTED", "value": "Sample Rejected"} + ] +} diff --git a/app/Libraries/Data/test_type.json b/app/Libraries/Data/test_type.json index 5bf50e1..f120357 100755 --- a/app/Libraries/Data/test_type.json +++ b/app/Libraries/Data/test_type.json @@ -1,11 +1,11 @@ -{"name": "test_type", - "VSName": "Test Type", - "VCategory": "System", - "values": [ - {"key": "TEST", "value": "Test"}, - {"key": "PARAM", "value": "Parameter"}, - {"key": "CALC", "value": "Calculated Test"}, - {"key": "GROUP", "value": "Group Test"}, - {"key": "TITLE", "value": "Title"} - ] -} +{"name": "test_type", + "VSName": "Test Type", + "VCategory": "System", + "values": [ + {"key": "TEST", "value": "Test"}, + {"key": "PARAM", "value": "Parameter"}, + {"key": "CALC", "value": "Calculated Test"}, + {"key": "GROUP", "value": "Group Test"}, + {"key": "TITLE", "value": "Title"} + ] +} diff --git a/app/Libraries/Data/text_ref_type.json b/app/Libraries/Data/text_ref_type.json index 9dfef96..7566d59 100755 --- a/app/Libraries/Data/text_ref_type.json +++ b/app/Libraries/Data/text_ref_type.json @@ -1,8 +1,8 @@ -{"name": "text_ref_type", - "VSName": "Text Reference Type", - "VCategory": "System", - "values": [ - {"key": "VSET", "value": "Value Set"}, - {"key": "TEXT", "value": "Text"} - ] -} +{"name": "text_ref_type", + "VSName": "Text Reference Type", + "VCategory": "System", + "values": [ + {"key": "VSET", "value": "Value Set"}, + {"key": "TEXT", "value": "Text"} + ] +} diff --git a/app/Libraries/Data/unit.json b/app/Libraries/Data/unit.json index 9527c1f..027ac69 100755 --- a/app/Libraries/Data/unit.json +++ b/app/Libraries/Data/unit.json @@ -1,9 +1,9 @@ -{"name": "unit", - "VSName": "Unit", - "VCategory": "System", - "values": [ - {"key": "L", "value": "Liter"}, - {"key": "mL", "value": "Mili Liter"}, - {"key": "Pcs", "value": "Pieces"} - ] -} +{"name": "unit", + "VSName": "Unit", + "VCategory": "System", + "values": [ + {"key": "L", "value": "Liter"}, + {"key": "mL", "value": "Mili Liter"}, + {"key": "Pcs", "value": "Pieces"} + ] +} diff --git a/app/Models/Contact/ContactDetailModel.php b/app/Models/Contact/ContactDetailModel.php index a511f28..cd5130d 100755 --- a/app/Models/Contact/ContactDetailModel.php +++ b/app/Models/Contact/ContactDetailModel.php @@ -1,9 +1,9 @@ -where('ContactID', $ContactID) - ->where('SiteID', $detail['SiteID']) - ->first(); - - if ($existing) { - $this->update($existing[$this->primaryKey], $detail); - } else { - $this->insert($detail); - } - - $keptSiteIDs[] = $detail['SiteID']; - } - - // Delete missing rows - if (!empty($keptSiteIDs)) { - $this->where('ContactID', $ContactID) - ->whereNotIn('SiteID', $keptSiteIDs) - ->delete(); - } else { - $this->where('ContactID', $ContactID)->delete(); + + foreach ($contactDetails as $detail) { + if (empty($detail['SiteID'])) { + continue; + } + + $detail['ContactID'] = $ContactID; + + $existing = $this->where('ContactID', $ContactID) + ->where('SiteID', $detail['SiteID']) + ->first(); + + if ($existing) { + $this->update($existing[$this->primaryKey], $detail); + } else { + $this->insert($detail); + } + + $keptSiteIDs[] = $detail['SiteID']; + } + + // Delete missing rows + if (!empty($keptSiteIDs)) { + $this->where('ContactID', $ContactID) + ->whereNotIn('SiteID', $keptSiteIDs) + ->delete(); + } else { + $this->where('ContactID', $ContactID)->delete(); } return [ @@ -55,10 +55,10 @@ class ContactDetailModel extends BaseModel { ]; } catch (\Throwable $e) { log_message('error', 'syncDetails error: ' . $e->getMessage()); - - return [ - 'status' => 'error', - 'message' => $e->getMessage(), + + return [ + 'status' => 'error', + 'message' => $e->getMessage(), ]; } } @@ -113,6 +113,7 @@ class ContactDetailModel extends BaseModel { $existing = $this->where('ContactDetID', (int) $detailID) ->where('ContactID', $contactID) + ->where('ContactEndDate', null) ->first(); if (empty($existing)) { @@ -122,13 +123,8 @@ class ContactDetailModel extends BaseModel { $updateData = array_intersect_key($detail, array_flip($this->allowedFields)); unset($updateData['ContactID']); - if ($updateData !== []) { - $db = \Config\Database::connect(); - $db->table($this->table) - ->where('ContactDetID', (int) $detailID) - ->where('ContactID', $contactID) - ->where('ContactEndDate', null) - ->update($updateData); + if ($updateData !== [] && !$this->update((int) $detailID, $updateData)) { + return false; } } diff --git a/app/Models/Organization/CodingSysModel.php b/app/Models/Organization/CodingSysModel.php index 5a2ed9b..2be3acd 100755 --- a/app/Models/Organization/CodingSysModel.php +++ b/app/Models/Organization/CodingSysModel.php @@ -1,17 +1,17 @@ - 'required|min_length[3]|max_length[50]', - 'Email' => 'required|valid_email|max_length[100]', - ]; - - protected $validationMessages = [ - 'Username' => [ - 'required' => 'Username is required', - 'min_length' => 'Username must be at least 3 characters', - 'max_length' => 'Username cannot exceed 50 characters', - ], - 'Email' => [ - 'required' => 'Email is required', - 'valid_email' => 'Please provide a valid email address', - 'max_length' => 'Email cannot exceed 100 characters', - ], - ]; - - /** - * Get active users only - */ - public function getActive() - { - return $this->where('DelDate', null) - ->where('IsActive', true) - ->findAll(); - } - - /** - * Find user by username - */ - public function findByUsername($username) - { - return $this->where('Username', $username) - ->where('DelDate', null) - ->first(); - } - - /** - * Find user by email - */ - public function findByEmail($email) - { - return $this->where('Email', $email) - ->where('DelDate', null) - ->first(); - } - - /** - * Search users by name, username, or email - */ - public function search($term) - { - return $this->where('DelDate', null) - ->groupStart() - ->like('Username', $term) - ->orLike('Email', $term) - ->orLike('Name', $term) - ->groupEnd() - ->findAll(); - } - - /** - * Get users by role - */ - public function getByRole($role) - { - return $this->where('Role', $role) - ->where('DelDate', null) - ->where('IsActive', true) - ->findAll(); - } - - /** - * Get users by department - */ - public function getByDepartment($department) - { - return $this->where('Department', $department) - ->where('DelDate', null) - ->where('IsActive', true) - ->findAll(); - } - - /** - * Soft delete user - */ - public function softDelete($id) - { - return $this->update($id, [ - 'DelDate' => date('Y-m-d H:i:s'), - 'IsActive' => false - ]); - } - - /** - * Restore soft-deleted user - */ - public function restore($id) - { - return $this->update($id, [ - 'DelDate' => null, - 'IsActive' => true - ]); - } + 'required|min_length[3]|max_length[50]', + 'Email' => 'required|valid_email|max_length[100]', + ]; + + protected $validationMessages = [ + 'Username' => [ + 'required' => 'Username is required', + 'min_length' => 'Username must be at least 3 characters', + 'max_length' => 'Username cannot exceed 50 characters', + ], + 'Email' => [ + 'required' => 'Email is required', + 'valid_email' => 'Please provide a valid email address', + 'max_length' => 'Email cannot exceed 100 characters', + ], + ]; + + /** + * Get active users only + */ + public function getActive() + { + return $this->where('DelDate', null) + ->where('IsActive', true) + ->findAll(); + } + + /** + * Find user by username + */ + public function findByUsername($username) + { + return $this->where('Username', $username) + ->where('DelDate', null) + ->first(); + } + + /** + * Find user by email + */ + public function findByEmail($email) + { + return $this->where('Email', $email) + ->where('DelDate', null) + ->first(); + } + + /** + * Search users by name, username, or email + */ + public function search($term) + { + return $this->where('DelDate', null) + ->groupStart() + ->like('Username', $term) + ->orLike('Email', $term) + ->orLike('Name', $term) + ->groupEnd() + ->findAll(); + } + + /** + * Get users by role + */ + public function getByRole($role) + { + return $this->where('Role', $role) + ->where('DelDate', null) + ->where('IsActive', true) + ->findAll(); + } + + /** + * Get users by department + */ + public function getByDepartment($department) + { + return $this->where('Department', $department) + ->where('DelDate', null) + ->where('IsActive', true) + ->findAll(); + } + + /** + * Soft delete user + */ + public function softDelete($id) + { + return $this->update($id, [ + 'DelDate' => date('Y-m-d H:i:s'), + 'IsActive' => false + ]); + } + + /** + * Restore soft-deleted user + */ + public function restore($id) + { + return $this->update($id, [ + 'DelDate' => null, + 'IsActive' => true + ]); + } } \ No newline at end of file diff --git a/app/Services/AuditLogService.php b/app/Services/AuditLogService.php index 073465e..361e6ec 100755 --- a/app/Services/AuditLogService.php +++ b/app/Services/AuditLogService.php @@ -1,179 +1,179 @@ - 'logpatient', - 'patient' => 'logpatient', - 'visit' => 'logpatient', - 'logorder' => 'logorder', - 'order' => 'logorder', - 'specimen' => 'logorder', - 'result' => 'logorder', - 'logmaster' => 'logmaster', - 'master' => 'logmaster', - 'config' => 'logmaster', - 'valueset' => 'logmaster', - 'logsystem' => 'logsystem', - 'system' => 'logsystem', - 'auth' => 'logsystem', - 'job' => 'logsystem', - ]; - - private const PRIMARY_KEYS = [ - 'logpatient' => 'LogPatientID', - 'logorder' => 'LogOrderID', - 'logmaster' => 'LogMasterID', - 'logsystem' => 'LogSystemID', - ]; - - private const DEFAULT_PAGE = 1; - private const DEFAULT_PER_PAGE = 20; - private const MAX_PER_PAGE = 100; - - private static ?BaseConnection $db = null; - - public function fetchLogs(array $filters): array - { - $tableKey = $filters['table'] ?? null; - if (empty($tableKey)) { - throw new InvalidArgumentException('table parameter is required'); - } - - $logTable = $this->resolveLogTable($tableKey); - if ($logTable === null) { - throw new InvalidArgumentException("Unknown audit table: {$tableKey}"); - } - - $builder = $this->getDb()->table($logTable); - - $this->applyFilters($builder, $filters); - - $total = (int) $builder->countAllResults(false); - - $page = $this->normalizePage($filters['page'] ?? null); - $perPage = $this->normalizePerPage($filters['perPage'] ?? $filters['per_page'] ?? null); - $offset = ($page - 1) * $perPage; - - $builder->orderBy('LogDate', 'DESC'); - $builder->orderBy($this->getPrimaryKey($logTable), 'DESC'); - - $rows = $builder - ->limit($perPage, $offset) - ->get() - ->getResultArray(); - - return [ - 'data' => $rows, - 'pagination' => [ - 'page' => $page, - 'perPage' => $perPage, - 'total' => $total, - ], - ]; - } - - private function applyFilters($builder, array $filters): void - { - if (!empty($filters['rec_id'])) { - $builder->where('RecID', (string) $filters['rec_id']); - } - - if (!empty($filters['event_id'])) { - $builder->where('EventID', $this->normalizeCode($filters['event_id'])); - } - - if (!empty($filters['activity_id'])) { - $builder->where('ActivityID', $this->normalizeCode($filters['activity_id'])); - } - - $this->applyDateRange($builder, $filters['from'] ?? null, $filters['to'] ?? null); - - if (!empty($filters['search'])) { - $search = trim($filters['search']); - if ($search !== '') { - $builder->groupStart(); - $builder->like('UserID', $search); - $builder->orLike('Reason', $search); - $builder->orLike('FldName', $search); - $builder->orLike('FldValuePrev', $search); - $builder->orLike('FldValueNew', $search); - $builder->orLike('EventID', $search); - $builder->orLike('ActivityID', $search); - $builder->groupEnd(); - } - } - } - - private function applyDateRange($builder, ?string $from, ?string $to): void - { - if ($from !== null && trim($from) !== '') { - $builder->where('LogDate >=', $this->normalizeDate($from)); - } - if ($to !== null && trim($to) !== '') { - $builder->where('LogDate <=', $this->normalizeDate($to)); - } - } - - private function normalizeDate(string $value): string - { - try { - $dt = new DateTime($value, new DateTimeZone('UTC')); - } catch (Throwable $e) { - throw new InvalidArgumentException('Invalid date: ' . $value); - } - - return $dt->format('Y-m-d H:i:s'); - } - - private function normalizeCode(string $value): string - { - return strtoupper(trim($value)); - } - - private function normalizePage($value): int - { - $page = (int) ($value ?? self::DEFAULT_PAGE); - return $page < 1 ? self::DEFAULT_PAGE : $page; - } - - private function normalizePerPage($value): int - { - $perPage = (int) ($value ?? self::DEFAULT_PER_PAGE); - if ($perPage < 1) { - return self::DEFAULT_PER_PAGE; - } - if ($perPage > self::MAX_PER_PAGE) { - throw new InvalidArgumentException('perPage cannot be greater than ' . self::MAX_PER_PAGE); - } - return $perPage; - } - - private function resolveLogTable(?string $key): ?string - { - if ($key === null) { - return null; - } - - $lookup = strtolower(trim($key)); - return self::TABLE_MAP[$lookup] ?? null; - } - - private function getPrimaryKey(string $table): string - { - return self::PRIMARY_KEYS[$table] ?? 'LogID'; - } - - private function getDb(): BaseConnection - { - return self::$db ??= \Config\Database::connect(); - } -} + 'logpatient', + 'patient' => 'logpatient', + 'visit' => 'logpatient', + 'logorder' => 'logorder', + 'order' => 'logorder', + 'specimen' => 'logorder', + 'result' => 'logorder', + 'logmaster' => 'logmaster', + 'master' => 'logmaster', + 'config' => 'logmaster', + 'valueset' => 'logmaster', + 'logsystem' => 'logsystem', + 'system' => 'logsystem', + 'auth' => 'logsystem', + 'job' => 'logsystem', + ]; + + private const PRIMARY_KEYS = [ + 'logpatient' => 'LogPatientID', + 'logorder' => 'LogOrderID', + 'logmaster' => 'LogMasterID', + 'logsystem' => 'LogSystemID', + ]; + + private const DEFAULT_PAGE = 1; + private const DEFAULT_PER_PAGE = 20; + private const MAX_PER_PAGE = 100; + + private static ?BaseConnection $db = null; + + public function fetchLogs(array $filters): array + { + $tableKey = $filters['table'] ?? null; + if (empty($tableKey)) { + throw new InvalidArgumentException('table parameter is required'); + } + + $logTable = $this->resolveLogTable($tableKey); + if ($logTable === null) { + throw new InvalidArgumentException("Unknown audit table: {$tableKey}"); + } + + $builder = $this->getDb()->table($logTable); + + $this->applyFilters($builder, $filters); + + $total = (int) $builder->countAllResults(false); + + $page = $this->normalizePage($filters['page'] ?? null); + $perPage = $this->normalizePerPage($filters['perPage'] ?? $filters['per_page'] ?? null); + $offset = ($page - 1) * $perPage; + + $builder->orderBy('LogDate', 'DESC'); + $builder->orderBy($this->getPrimaryKey($logTable), 'DESC'); + + $rows = $builder + ->limit($perPage, $offset) + ->get() + ->getResultArray(); + + return [ + 'data' => $rows, + 'pagination' => [ + 'page' => $page, + 'perPage' => $perPage, + 'total' => $total, + ], + ]; + } + + private function applyFilters($builder, array $filters): void + { + if (!empty($filters['rec_id'])) { + $builder->where('RecID', (string) $filters['rec_id']); + } + + if (!empty($filters['event_id'])) { + $builder->where('EventID', $this->normalizeCode($filters['event_id'])); + } + + if (!empty($filters['activity_id'])) { + $builder->where('ActivityID', $this->normalizeCode($filters['activity_id'])); + } + + $this->applyDateRange($builder, $filters['from'] ?? null, $filters['to'] ?? null); + + if (!empty($filters['search'])) { + $search = trim($filters['search']); + if ($search !== '') { + $builder->groupStart(); + $builder->like('UserID', $search); + $builder->orLike('Reason', $search); + $builder->orLike('FldName', $search); + $builder->orLike('FldValuePrev', $search); + $builder->orLike('FldValueNew', $search); + $builder->orLike('EventID', $search); + $builder->orLike('ActivityID', $search); + $builder->groupEnd(); + } + } + } + + private function applyDateRange($builder, ?string $from, ?string $to): void + { + if ($from !== null && trim($from) !== '') { + $builder->where('LogDate >=', $this->normalizeDate($from)); + } + if ($to !== null && trim($to) !== '') { + $builder->where('LogDate <=', $this->normalizeDate($to)); + } + } + + private function normalizeDate(string $value): string + { + try { + $dt = new DateTime($value, new DateTimeZone('UTC')); + } catch (Throwable $e) { + throw new InvalidArgumentException('Invalid date: ' . $value); + } + + return $dt->format('Y-m-d H:i:s'); + } + + private function normalizeCode(string $value): string + { + return strtoupper(trim($value)); + } + + private function normalizePage($value): int + { + $page = (int) ($value ?? self::DEFAULT_PAGE); + return $page < 1 ? self::DEFAULT_PAGE : $page; + } + + private function normalizePerPage($value): int + { + $perPage = (int) ($value ?? self::DEFAULT_PER_PAGE); + if ($perPage < 1) { + return self::DEFAULT_PER_PAGE; + } + if ($perPage > self::MAX_PER_PAGE) { + throw new InvalidArgumentException('perPage cannot be greater than ' . self::MAX_PER_PAGE); + } + return $perPage; + } + + private function resolveLogTable(?string $key): ?string + { + if ($key === null) { + return null; + } + + $lookup = strtolower(trim($key)); + return self::TABLE_MAP[$lookup] ?? null; + } + + private function getPrimaryKey(string $table): string + { + return self::PRIMARY_KEYS[$table] ?? 'LogID'; + } + + private function getDb(): BaseConnection + { + return self::$db ??= \Config\Database::connect(); + } +} diff --git a/app/Services/AuditService.php b/app/Services/AuditService.php index cab8c3c..23f352e 100755 --- a/app/Services/AuditService.php +++ b/app/Services/AuditService.php @@ -1,346 +1,346 @@ - 'logpatient', - 'patient' => 'logpatient', - 'visit' => 'logpatient', - 'logorder' => 'logorder', - 'order' => 'logorder', - 'specimen' => 'logorder', - 'result' => 'logorder', - 'logmaster' => 'logmaster', - 'master' => 'logmaster', - 'config' => 'logmaster', - 'valueset' => 'logmaster', - 'logsystem' => 'logsystem', - 'system' => 'logsystem', - 'auth' => 'logsystem', - 'job' => 'logsystem', - ]; - - private const DEFAULT_APP_ID = 'clqms-api'; - private const ENTITY_VERSION_DEFAULT = 1; - - private static ?BaseConnection $db = null; - private static $session = null; - private static ?IncomingRequest $request = null; - private static ?array $eventIdCache = null; - private static ?string $cachedRequestId = null; - - public static function logData( - string $eventId, - string $activityId, - string $entityType, - string $entityId, - string $tableName, - ?string $fldName = null, - $previousValue = null, - $newValue = null, - ?string $reason = null, - ?array $context = null, - array $options = [] - ): void { - $sourceTable = $tableName ?: $entityType; - $logTable = self::resolveLogTable($sourceTable); - if ($logTable === null) { - log_message('warning', "AuditService cannot resolve log table for {$sourceTable}"); - return; - } - - $record = self::buildRecord( - $logTable, - self::normalizeEventId($eventId), - strtoupper($activityId), - $entityType, - $entityId, - $sourceTable, - $fldName, - $previousValue, - $newValue, - $reason, - $context, - $options - ); - - if ($record === null) { - return; - } - - try { - self::getDb()->table($logTable)->insert($record); - } catch (\Throwable $e) { - log_message('error', "AuditService failed to insert into {$logTable}: {$e->getMessage()}"); - } - } - - private static function buildRecord( - string $logTable, - string $eventId, - string $activityId, - string $entityType, - string $entityId, - string $tblName, - ?string $fldName, - $previousValue, - $newValue, - ?string $reason, - ?array $context, - array $options - ): ?array { - $contextJson = self::buildContext($context, $options, $entityType); - if ($contextJson === null) { - return null; - } - - return [ - 'TblName' => $tblName, - 'RecID' => (string) $entityId, - 'FldName' => $fldName, - 'FldValuePrev' => self::serializeValue($previousValue), - 'FldValueNew' => self::serializeValue($newValue), - 'UserID' => self::resolveUserId($options), - 'SiteID' => self::resolveSiteId($options), - 'DIDType' => $options['did_type'] ?? null, - 'DID' => $options['did'] ?? null, - 'MachineID' => $options['machine_id'] ?? gethostname(), - 'SessionID' => self::resolveSessionId($options), - 'AppID' => $options['app_id'] ?? self::DEFAULT_APP_ID, - 'ProcessID' => $options['process_id'] ?? null, - 'WebPageID' => $options['web_page_id'] ?? self::resolveRoute($options), - 'EventID' => $eventId, - 'ActivityID' => $activityId, - 'Reason' => $reason, - 'LogDate' => self::nowWithMillis(), - 'Context' => $contextJson, - 'IpAddress' => self::resolveIpAddress(), - ]; - } - - private static function serializeValue($value): ?string - { - if ($value === null) { - return null; - } - - if (is_scalar($value)) { - return (string) $value; - } - - $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - return $json !== false ? $json : null; - } - - private static function buildContext(?array $context, array $options, string $entityType): ?string - { - $route = $options['route'] ?? self::resolveRoute($options); - $payload = array_merge( - [ - 'request_id' => $options['request_id'] ?? self::resolveRequestId(), - 'route' => $route, - 'timestamp_utc' => $options['timestamp_utc'] ?? self::timestampUtc(), - 'entity_type' => $options['entity_type'] ?? $entityType, - 'entity_version' => $options['entity_version'] ?? self::ENTITY_VERSION_DEFAULT, - ], - $context ?? [] - ); - - $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - return $json !== false ? $json : null; - } - - private static function resolveLogTable(?string $source): ?string - { - if ($source === null) { - return null; - } - - $key = strtolower(trim($source)); - return self::TABLE_MAP[$key] ?? null; - } - - private static function resolveUserId(array $options): string - { - return $options['user_id'] ?? self::getSessionValue('user_id') ?? 'SYSTEM'; - } - - private static function resolveSiteId(array $options): string - { - return $options['site_id'] ?? self::getSessionValue('site_id') ?? 'GLOBAL'; - } - - private static function resolveSessionId(array $options): string - { - if (!empty($options['session_id'])) { - return $options['session_id']; - } - - $session = self::getSession(); - if ($session !== null && method_exists($session, 'getId')) { - $id = $session->getId(); - if (!empty($id)) { - return $id; - } - } - - if (session_status() === PHP_SESSION_ACTIVE) { - $id = session_id(); - if (!empty($id)) { - return $id; - } - } - - return self::generateUniqueId('sess'); - } - - private static function resolveRoute(array $options): string - { - if (!empty($options['route'])) { - return $options['route']; - } - - $request = self::getRequest(); - if ($request !== null) { - return trim(sprintf('%s %s', $request->getMethod(), $request->getUri()->getPath())); - } - - return 'cli'; - } - - private static function resolveIpAddress(): ?string - { - $request = self::getRequest(); - if ($request !== null) { - return $request->getIPAddress(); - } - - return $_SERVER['REMOTE_ADDR'] ?? null; - } - - private static function normalizeEventId(string $eventId): string - { - $normalized = strtoupper(trim($eventId)); - if (empty($normalized)) { - log_message('warning', 'AuditService received empty EventID'); - return $eventId; - } - - if (!self::isKnownEvent($normalized)) { - log_message('warning', "AuditService unknown EventID: {$normalized}"); - } - - return $normalized; - } - - private static function isKnownEvent(string $eventId): bool - { - if (self::$eventIdCache === null) { - $raw = ValueSet::getRaw('event_id') ?? []; - self::$eventIdCache = array_filter(array_map(fn ($item) => $item['key'] ?? null, $raw)); - } - - return in_array($eventId, self::$eventIdCache, true); - } - - private static function resolveRequestId(): string - { - if (self::$cachedRequestId !== null) { - return self::$cachedRequestId; - } - - $request = self::getRequest(); - if ($request !== null) { - $value = $request->getHeaderLine('X-Request-ID'); - if (!empty($value)) { - self::$cachedRequestId = $value; - return $value; - } - } - - foreach (['HTTP_X_REQUEST_ID', 'REQUEST_ID'] as $header) { - if (!empty($_SERVER[$header])) { - self::$cachedRequestId = $_SERVER[$header]; - return self::$cachedRequestId; - } - } - - return self::$cachedRequestId = self::generateUniqueId('req'); - } - - private static function nowWithMillis(): string - { - $dt = new DateTime('now', new DateTimeZone('UTC')); - return $dt->format('Y-m-d H:i:s.v'); - } - - private static function timestampUtc(): string - { - $dt = new DateTime('now', new DateTimeZone('UTC')); - return $dt->format('Y-m-d\TH:i:s.v\Z'); - } - - private static function getSessionValue(string $key): ?string - { - $session = self::getSession(); - if ($session === null) { - return null; - } - - if (!method_exists($session, 'get')) { - return null; - } - - $value = $session->get($key); - return $value !== null ? (string) $value : null; - } - - private static function getSession(): ?object - { - if (self::$session !== null) { - return self::$session; - } - - try { - return self::$session = Services::session(); - } catch (\Throwable $e) { - return self::$session = null; - } - } - - private static function getRequest(): ?IncomingRequest - { - if (self::$request !== null) { - return self::$request; - } - - try { - return self::$request = Services::request(); - } catch (\Throwable $e) { - return self::$request = null; - } - } - - private static function getDb(): BaseConnection - { - return self::$db ??= \Config\Database::connect(); - } - - private static function generateUniqueId(string $prefix): string - { - try { - return $prefix . '_' . bin2hex(random_bytes(8)); - } catch (\Throwable $e) { - return uniqid("{$prefix}_", true); - } - } -} + 'logpatient', + 'patient' => 'logpatient', + 'visit' => 'logpatient', + 'logorder' => 'logorder', + 'order' => 'logorder', + 'specimen' => 'logorder', + 'result' => 'logorder', + 'logmaster' => 'logmaster', + 'master' => 'logmaster', + 'config' => 'logmaster', + 'valueset' => 'logmaster', + 'logsystem' => 'logsystem', + 'system' => 'logsystem', + 'auth' => 'logsystem', + 'job' => 'logsystem', + ]; + + private const DEFAULT_APP_ID = 'clqms-api'; + private const ENTITY_VERSION_DEFAULT = 1; + + private static ?BaseConnection $db = null; + private static $session = null; + private static ?IncomingRequest $request = null; + private static ?array $eventIdCache = null; + private static ?string $cachedRequestId = null; + + public static function logData( + string $eventId, + string $activityId, + string $entityType, + string $entityId, + string $tableName, + ?string $fldName = null, + $previousValue = null, + $newValue = null, + ?string $reason = null, + ?array $context = null, + array $options = [] + ): void { + $sourceTable = $tableName ?: $entityType; + $logTable = self::resolveLogTable($sourceTable); + if ($logTable === null) { + log_message('warning', "AuditService cannot resolve log table for {$sourceTable}"); + return; + } + + $record = self::buildRecord( + $logTable, + self::normalizeEventId($eventId), + strtoupper($activityId), + $entityType, + $entityId, + $sourceTable, + $fldName, + $previousValue, + $newValue, + $reason, + $context, + $options + ); + + if ($record === null) { + return; + } + + try { + self::getDb()->table($logTable)->insert($record); + } catch (\Throwable $e) { + log_message('error', "AuditService failed to insert into {$logTable}: {$e->getMessage()}"); + } + } + + private static function buildRecord( + string $logTable, + string $eventId, + string $activityId, + string $entityType, + string $entityId, + string $tblName, + ?string $fldName, + $previousValue, + $newValue, + ?string $reason, + ?array $context, + array $options + ): ?array { + $contextJson = self::buildContext($context, $options, $entityType); + if ($contextJson === null) { + return null; + } + + return [ + 'TblName' => $tblName, + 'RecID' => (string) $entityId, + 'FldName' => $fldName, + 'FldValuePrev' => self::serializeValue($previousValue), + 'FldValueNew' => self::serializeValue($newValue), + 'UserID' => self::resolveUserId($options), + 'SiteID' => self::resolveSiteId($options), + 'DIDType' => $options['did_type'] ?? null, + 'DID' => $options['did'] ?? null, + 'MachineID' => $options['machine_id'] ?? gethostname(), + 'SessionID' => self::resolveSessionId($options), + 'AppID' => $options['app_id'] ?? self::DEFAULT_APP_ID, + 'ProcessID' => $options['process_id'] ?? null, + 'WebPageID' => $options['web_page_id'] ?? self::resolveRoute($options), + 'EventID' => $eventId, + 'ActivityID' => $activityId, + 'Reason' => $reason, + 'LogDate' => self::nowWithMillis(), + 'Context' => $contextJson, + 'IpAddress' => self::resolveIpAddress(), + ]; + } + + private static function serializeValue($value): ?string + { + if ($value === null) { + return null; + } + + if (is_scalar($value)) { + return (string) $value; + } + + $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return $json !== false ? $json : null; + } + + private static function buildContext(?array $context, array $options, string $entityType): ?string + { + $route = $options['route'] ?? self::resolveRoute($options); + $payload = array_merge( + [ + 'request_id' => $options['request_id'] ?? self::resolveRequestId(), + 'route' => $route, + 'timestamp_utc' => $options['timestamp_utc'] ?? self::timestampUtc(), + 'entity_type' => $options['entity_type'] ?? $entityType, + 'entity_version' => $options['entity_version'] ?? self::ENTITY_VERSION_DEFAULT, + ], + $context ?? [] + ); + + $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return $json !== false ? $json : null; + } + + private static function resolveLogTable(?string $source): ?string + { + if ($source === null) { + return null; + } + + $key = strtolower(trim($source)); + return self::TABLE_MAP[$key] ?? null; + } + + private static function resolveUserId(array $options): string + { + return $options['user_id'] ?? self::getSessionValue('user_id') ?? 'SYSTEM'; + } + + private static function resolveSiteId(array $options): string + { + return $options['site_id'] ?? self::getSessionValue('site_id') ?? 'GLOBAL'; + } + + private static function resolveSessionId(array $options): string + { + if (!empty($options['session_id'])) { + return $options['session_id']; + } + + $session = self::getSession(); + if ($session !== null && method_exists($session, 'getId')) { + $id = $session->getId(); + if (!empty($id)) { + return $id; + } + } + + if (session_status() === PHP_SESSION_ACTIVE) { + $id = session_id(); + if (!empty($id)) { + return $id; + } + } + + return self::generateUniqueId('sess'); + } + + private static function resolveRoute(array $options): string + { + if (!empty($options['route'])) { + return $options['route']; + } + + $request = self::getRequest(); + if ($request !== null) { + return trim(sprintf('%s %s', $request->getMethod(), $request->getUri()->getPath())); + } + + return 'cli'; + } + + private static function resolveIpAddress(): ?string + { + $request = self::getRequest(); + if ($request !== null) { + return $request->getIPAddress(); + } + + return $_SERVER['REMOTE_ADDR'] ?? null; + } + + private static function normalizeEventId(string $eventId): string + { + $normalized = strtoupper(trim($eventId)); + if (empty($normalized)) { + log_message('warning', 'AuditService received empty EventID'); + return $eventId; + } + + if (!self::isKnownEvent($normalized)) { + log_message('warning', "AuditService unknown EventID: {$normalized}"); + } + + return $normalized; + } + + private static function isKnownEvent(string $eventId): bool + { + if (self::$eventIdCache === null) { + $raw = ValueSet::getRaw('event_id') ?? []; + self::$eventIdCache = array_filter(array_map(fn ($item) => $item['key'] ?? null, $raw)); + } + + return in_array($eventId, self::$eventIdCache, true); + } + + private static function resolveRequestId(): string + { + if (self::$cachedRequestId !== null) { + return self::$cachedRequestId; + } + + $request = self::getRequest(); + if ($request !== null) { + $value = $request->getHeaderLine('X-Request-ID'); + if (!empty($value)) { + self::$cachedRequestId = $value; + return $value; + } + } + + foreach (['HTTP_X_REQUEST_ID', 'REQUEST_ID'] as $header) { + if (!empty($_SERVER[$header])) { + self::$cachedRequestId = $_SERVER[$header]; + return self::$cachedRequestId; + } + } + + return self::$cachedRequestId = self::generateUniqueId('req'); + } + + private static function nowWithMillis(): string + { + $dt = new DateTime('now', new DateTimeZone('UTC')); + return $dt->format('Y-m-d H:i:s.v'); + } + + private static function timestampUtc(): string + { + $dt = new DateTime('now', new DateTimeZone('UTC')); + return $dt->format('Y-m-d\TH:i:s.v\Z'); + } + + private static function getSessionValue(string $key): ?string + { + $session = self::getSession(); + if ($session === null) { + return null; + } + + if (!method_exists($session, 'get')) { + return null; + } + + $value = $session->get($key); + return $value !== null ? (string) $value : null; + } + + private static function getSession(): ?object + { + if (self::$session !== null) { + return self::$session; + } + + try { + return self::$session = Services::session(); + } catch (\Throwable $e) { + return self::$session = null; + } + } + + private static function getRequest(): ?IncomingRequest + { + if (self::$request !== null) { + return self::$request; + } + + try { + return self::$request = Services::request(); + } catch (\Throwable $e) { + return self::$request = null; + } + } + + private static function getDb(): BaseConnection + { + return self::$db ??= \Config\Database::connect(); + } + + private static function generateUniqueId(string $prefix): string + { + try { + return $prefix . '_' . bin2hex(random_bytes(8)); + } catch (\Throwable $e) { + return uniqid("{$prefix}_", true); + } + } +} diff --git a/app/Services/RuleEngineService.php b/app/Services/RuleEngineService.php index 8a04378..f66dc53 100755 --- a/app/Services/RuleEngineService.php +++ b/app/Services/RuleEngineService.php @@ -1,389 +1,389 @@ -ruleDefModel = new RuleDefModel(); - $this->expr = new RuleExpressionService(); - } - - /** - * Run rules for an event. - * - * Expected context keys: - * - order: array (must include InternalOID) - * - patient: array (optional, includes Sex, Age, etc.) - * - tests: array (optional, patres rows) - * - * @param string $eventCode The event that triggered rule execution - * @param array $context Context data for rule evaluation - * @return void - */ - public function run(string $eventCode, array $context = []): void - { - $order = $context['order'] ?? null; - $testSiteID = $context['testSiteID'] ?? null; - - if (is_array($order) && isset($order['TestSiteID']) && $testSiteID === null) { - $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null; - } - - $rules = $this->ruleDefModel->getActiveByEvent($eventCode, $testSiteID); - if (empty($rules)) { - return; - } - - foreach ($rules as $rule) { - $rid = (int) ($rule['RuleID'] ?? 0); - if ($rid === 0) { - continue; - } - - try { - // Rules must have compiled expressions - $compiled = null; - if (!empty($rule['ConditionExprCompiled'])) { - $compiled = json_decode($rule['ConditionExprCompiled'], true); - } - - if (empty($compiled) || !is_array($compiled)) { - log_message('warning', 'Rule ' . $rid . ' has no compiled expression, skipping'); - continue; - } - - // Evaluate condition - $conditionExpr = $compiled['conditionExpr'] ?? 'true'; - $matches = $this->evaluateCondition($conditionExpr, $context); - - if (!$matches) { - // Execute else actions - $actions = $compiled['else'] ?? []; - } else { - // Execute then actions - $actions = $compiled['then'] ?? []; - } - - // Execute all actions - foreach ($actions as $action) { - $this->executeAction($action, $context); - } - } catch (\Throwable $e) { - log_message('error', 'Rule engine error (RuleID=' . $rid . '): ' . $e->getMessage()); - continue; - } - } - } - - /** - * Evaluate a condition expression - * Handles special functions like requested() by querying the database - */ - private function evaluateCondition(string $conditionExpr, array $context): bool - { - // Handle requested() function(s) by querying database - $conditionExpr = preg_replace_callback( - '/requested\s*\(\s*["\']([^"\']+)["\']\s*\)/i', - function (array $m) use ($context) { - $testCode = $m[1] ?? ''; - if ($testCode === '') { - return 'false'; - } - return $this->isTestRequested($testCode, $context) ? 'true' : 'false'; - }, - $conditionExpr - ); - - return $this->expr->evaluateBoolean($conditionExpr, $context); - } - - /** - * Check if a test was requested for the current order - */ - private function isTestRequested(string $testCode, array $context): bool - { - $order = $context['order'] ?? null; - if (!is_array($order) || empty($order['InternalOID'])) { - return false; - } - - $internalOID = (int) $order['InternalOID']; - $db = \Config\Database::connect(); - - // Query patres to check if test with given code exists - $result = $db->table('patres') - ->select('patres.*, testdefsite.TestSiteCode') - ->join('testdefsite', 'testdefsite.TestSiteID = patres.TestSiteID', 'inner') - ->where('patres.OrderID', $internalOID) - ->where('testdefsite.TestSiteCode', $testCode) - ->where('patres.DelDate', null) - ->where('testdefsite.EndDate', null) - ->get() - ->getRow(); - - return $result !== null; - } - - /** - * Execute an action based on its type - */ - protected function executeAction(array $action, array $context): void - { - $type = strtoupper((string) ($action['type'] ?? '')); - - switch ($type) { - case 'RESULT_SET': - case 'SET_RESULT': // legacy - $this->executeSetResult($action, $context); - break; - case 'TEST_INSERT': - case 'INSERT_TEST': // legacy - $this->executeInsertTest($action, $context); - break; - case 'TEST_DELETE': - $this->executeDeleteTest($action, $context); - break; - case 'COMMENT_INSERT': - case 'ADD_COMMENT': // legacy - $this->executeAddComment($action, $context); - break; - case 'NO_OP': - // Do nothing - break; - default: - log_message('warning', 'Unknown action type: ' . $type); - break; - } - } - - /** - * Execute SET_RESULT action - */ - protected function executeSetResult(array $action, array $context): void - { - $order = $context['order'] ?? null; - if (!is_array($order) || empty($order['InternalOID'])) { - throw new \Exception('SET_RESULT requires context.order.InternalOID'); - } - - $internalOID = (int) $order['InternalOID']; - $testSiteID = $context['testSiteID'] ?? null; - - if ($testSiteID === null && isset($order['TestSiteID'])) { - $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null; - } - - $testCode = $action['testCode'] ?? null; - if ($testCode !== null) { - $resolvedId = $this->resolveTestSiteIdByCode($testCode); - if ($resolvedId === null) { - throw new \Exception('SET_RESULT unknown test code: ' . $testCode); - } - $testSiteID = $resolvedId; - } - - if ($testSiteID === null) { - throw new \Exception('SET_RESULT requires testSiteID'); - } - - // Get the value - if (isset($action['valueExpr']) && is_string($action['valueExpr'])) { - $value = $this->expr->evaluate($action['valueExpr'], $context); - } else { - $value = $action['value'] ?? null; - } - - $testSiteCode = $testCode ?? $this->resolveTestSiteCode($testSiteID); - - $db = \Config\Database::connect(); - - // Check if patres row exists - $patres = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->where('DelDate', null) - ->get() - ->getRowArray(); - - if ($patres) { - // Update existing result - $ok = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->where('DelDate', null) - ->update(['Result' => $value]); - } else { - // Insert new result row - $ok = $db->table('patres')->insert([ - 'OrderID' => $internalOID, - 'TestSiteID' => $testSiteID, - 'TestSiteCode' => $testSiteCode, - 'Result' => $value, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - } - - if ($ok === false) { - throw new \Exception('SET_RESULT update/insert failed'); - } - } - - /** - * Execute INSERT_TEST action - Insert a new test into patres - */ - protected function executeInsertTest(array $action, array $context): void - { - $order = $context['order'] ?? null; - if (!is_array($order) || empty($order['InternalOID'])) { - throw new \Exception('INSERT_TEST requires context.order.InternalOID'); - } - - $internalOID = (int) $order['InternalOID']; - $testCode = $action['testCode'] ?? null; - - if (empty($testCode)) { - throw new \Exception('INSERT_TEST requires testCode'); - } - - // Look up TestSiteID from TestSiteCode - $testDefSiteModel = new TestDefSiteModel(); - $testSite = $testDefSiteModel->where('TestSiteCode', $testCode) - ->where('EndDate', null) - ->first(); - - if (!$testSite || empty($testSite['TestSiteID'])) { - throw new \Exception('INSERT_TEST: Test not found with code: ' . $testCode); - } - - $testSiteID = (int) $testSite['TestSiteID']; - - $db = \Config\Database::connect(); - - // Check if test already exists (avoid duplicates) - $existing = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->where('DelDate', null) - ->get() - ->getRow(); - - if ($existing) { - // Test already exists, skip - return; - } - - // Insert new test row - $ok = $db->table('patres')->insert([ - 'OrderID' => $internalOID, - 'TestSiteID' => $testSiteID, - 'TestSiteCode' => $testCode, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - if ($ok === false) { - throw new \Exception('INSERT_TEST insert failed'); - } - } - - /** - * Execute ADD_COMMENT action - Add a comment to the order - */ - protected function executeAddComment(array $action, array $context): void - { - $order = $context['order'] ?? null; - if (!is_array($order) || empty($order['InternalOID'])) { - throw new \Exception('ADD_COMMENT requires context.order.InternalOID'); - } - - $internalOID = (int) $order['InternalOID']; - $comment = $action['comment'] ?? null; - - if (empty($comment)) { - throw new \Exception('ADD_COMMENT requires comment'); - } - - $db = \Config\Database::connect(); - - // Insert comment into ordercom table - $ok = $db->table('ordercom')->insert([ - 'InternalOID' => $internalOID, - 'Comment' => $comment, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - if ($ok === false) { - throw new \Exception('ADD_COMMENT insert failed'); - } - } - - /** - * Execute TEST_DELETE action - soft delete a test from patres - */ - protected function executeDeleteTest(array $action, array $context): void - { - $order = $context['order'] ?? null; - if (!is_array($order) || empty($order['InternalOID'])) { - throw new \Exception('TEST_DELETE requires context.order.InternalOID'); - } - - $internalOID = (int) $order['InternalOID']; - $testCode = $action['testCode'] ?? null; - if (empty($testCode)) { - throw new \Exception('TEST_DELETE requires testCode'); - } - - $testDefSiteModel = new TestDefSiteModel(); - $testSite = $testDefSiteModel->where('TestSiteCode', $testCode) - ->where('EndDate', null) - ->first(); - - if (!$testSite || empty($testSite['TestSiteID'])) { - // Unknown test code: no-op - return; - } - - $testSiteID = (int) $testSite['TestSiteID']; - $db = \Config\Database::connect(); - - // Soft delete matching patres row(s) - $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->where('DelDate', null) - ->update(['DelDate' => date('Y-m-d H:i:s')]); - } - - private function resolveTestSiteCode(int $testSiteID): ?string - { - try { - $testDefSiteModel = new TestDefSiteModel(); - $row = $testDefSiteModel->where('TestSiteID', $testSiteID)->where('EndDate', null)->first(); - return $row['TestSiteCode'] ?? null; - } catch (\Throwable $e) { - return null; - } - } - - private function resolveTestSiteIdByCode(string $testSiteCode): ?int - { - try { - $testDefSiteModel = new TestDefSiteModel(); - $row = $testDefSiteModel->where('TestSiteCode', $testSiteCode)->where('EndDate', null)->first(); - if (empty($row['TestSiteID'])) { - return null; - } - - return (int) $row['TestSiteID']; - } catch (\Throwable $e) { - return null; - } - } -} +ruleDefModel = new RuleDefModel(); + $this->expr = new RuleExpressionService(); + } + + /** + * Run rules for an event. + * + * Expected context keys: + * - order: array (must include InternalOID) + * - patient: array (optional, includes Sex, Age, etc.) + * - tests: array (optional, patres rows) + * + * @param string $eventCode The event that triggered rule execution + * @param array $context Context data for rule evaluation + * @return void + */ + public function run(string $eventCode, array $context = []): void + { + $order = $context['order'] ?? null; + $testSiteID = $context['testSiteID'] ?? null; + + if (is_array($order) && isset($order['TestSiteID']) && $testSiteID === null) { + $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null; + } + + $rules = $this->ruleDefModel->getActiveByEvent($eventCode, $testSiteID); + if (empty($rules)) { + return; + } + + foreach ($rules as $rule) { + $rid = (int) ($rule['RuleID'] ?? 0); + if ($rid === 0) { + continue; + } + + try { + // Rules must have compiled expressions + $compiled = null; + if (!empty($rule['ConditionExprCompiled'])) { + $compiled = json_decode($rule['ConditionExprCompiled'], true); + } + + if (empty($compiled) || !is_array($compiled)) { + log_message('warning', 'Rule ' . $rid . ' has no compiled expression, skipping'); + continue; + } + + // Evaluate condition + $conditionExpr = $compiled['conditionExpr'] ?? 'true'; + $matches = $this->evaluateCondition($conditionExpr, $context); + + if (!$matches) { + // Execute else actions + $actions = $compiled['else'] ?? []; + } else { + // Execute then actions + $actions = $compiled['then'] ?? []; + } + + // Execute all actions + foreach ($actions as $action) { + $this->executeAction($action, $context); + } + } catch (\Throwable $e) { + log_message('error', 'Rule engine error (RuleID=' . $rid . '): ' . $e->getMessage()); + continue; + } + } + } + + /** + * Evaluate a condition expression + * Handles special functions like requested() by querying the database + */ + private function evaluateCondition(string $conditionExpr, array $context): bool + { + // Handle requested() function(s) by querying database + $conditionExpr = preg_replace_callback( + '/requested\s*\(\s*["\']([^"\']+)["\']\s*\)/i', + function (array $m) use ($context) { + $testCode = $m[1] ?? ''; + if ($testCode === '') { + return 'false'; + } + return $this->isTestRequested($testCode, $context) ? 'true' : 'false'; + }, + $conditionExpr + ); + + return $this->expr->evaluateBoolean($conditionExpr, $context); + } + + /** + * Check if a test was requested for the current order + */ + private function isTestRequested(string $testCode, array $context): bool + { + $order = $context['order'] ?? null; + if (!is_array($order) || empty($order['InternalOID'])) { + return false; + } + + $internalOID = (int) $order['InternalOID']; + $db = \Config\Database::connect(); + + // Query patres to check if test with given code exists + $result = $db->table('patres') + ->select('patres.*, testdefsite.TestSiteCode') + ->join('testdefsite', 'testdefsite.TestSiteID = patres.TestSiteID', 'inner') + ->where('patres.OrderID', $internalOID) + ->where('testdefsite.TestSiteCode', $testCode) + ->where('patres.DelDate', null) + ->where('testdefsite.EndDate', null) + ->get() + ->getRow(); + + return $result !== null; + } + + /** + * Execute an action based on its type + */ + protected function executeAction(array $action, array $context): void + { + $type = strtoupper((string) ($action['type'] ?? '')); + + switch ($type) { + case 'RESULT_SET': + case 'SET_RESULT': // legacy + $this->executeSetResult($action, $context); + break; + case 'TEST_INSERT': + case 'INSERT_TEST': // legacy + $this->executeInsertTest($action, $context); + break; + case 'TEST_DELETE': + $this->executeDeleteTest($action, $context); + break; + case 'COMMENT_INSERT': + case 'ADD_COMMENT': // legacy + $this->executeAddComment($action, $context); + break; + case 'NO_OP': + // Do nothing + break; + default: + log_message('warning', 'Unknown action type: ' . $type); + break; + } + } + + /** + * Execute SET_RESULT action + */ + protected function executeSetResult(array $action, array $context): void + { + $order = $context['order'] ?? null; + if (!is_array($order) || empty($order['InternalOID'])) { + throw new \Exception('SET_RESULT requires context.order.InternalOID'); + } + + $internalOID = (int) $order['InternalOID']; + $testSiteID = $context['testSiteID'] ?? null; + + if ($testSiteID === null && isset($order['TestSiteID'])) { + $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null; + } + + $testCode = $action['testCode'] ?? null; + if ($testCode !== null) { + $resolvedId = $this->resolveTestSiteIdByCode($testCode); + if ($resolvedId === null) { + throw new \Exception('SET_RESULT unknown test code: ' . $testCode); + } + $testSiteID = $resolvedId; + } + + if ($testSiteID === null) { + throw new \Exception('SET_RESULT requires testSiteID'); + } + + // Get the value + if (isset($action['valueExpr']) && is_string($action['valueExpr'])) { + $value = $this->expr->evaluate($action['valueExpr'], $context); + } else { + $value = $action['value'] ?? null; + } + + $testSiteCode = $testCode ?? $this->resolveTestSiteCode($testSiteID); + + $db = \Config\Database::connect(); + + // Check if patres row exists + $patres = $db->table('patres') + ->where('OrderID', $internalOID) + ->where('TestSiteID', $testSiteID) + ->where('DelDate', null) + ->get() + ->getRowArray(); + + if ($patres) { + // Update existing result + $ok = $db->table('patres') + ->where('OrderID', $internalOID) + ->where('TestSiteID', $testSiteID) + ->where('DelDate', null) + ->update(['Result' => $value]); + } else { + // Insert new result row + $ok = $db->table('patres')->insert([ + 'OrderID' => $internalOID, + 'TestSiteID' => $testSiteID, + 'TestSiteCode' => $testSiteCode, + 'Result' => $value, + 'CreateDate' => date('Y-m-d H:i:s'), + ]); + } + + if ($ok === false) { + throw new \Exception('SET_RESULT update/insert failed'); + } + } + + /** + * Execute INSERT_TEST action - Insert a new test into patres + */ + protected function executeInsertTest(array $action, array $context): void + { + $order = $context['order'] ?? null; + if (!is_array($order) || empty($order['InternalOID'])) { + throw new \Exception('INSERT_TEST requires context.order.InternalOID'); + } + + $internalOID = (int) $order['InternalOID']; + $testCode = $action['testCode'] ?? null; + + if (empty($testCode)) { + throw new \Exception('INSERT_TEST requires testCode'); + } + + // Look up TestSiteID from TestSiteCode + $testDefSiteModel = new TestDefSiteModel(); + $testSite = $testDefSiteModel->where('TestSiteCode', $testCode) + ->where('EndDate', null) + ->first(); + + if (!$testSite || empty($testSite['TestSiteID'])) { + throw new \Exception('INSERT_TEST: Test not found with code: ' . $testCode); + } + + $testSiteID = (int) $testSite['TestSiteID']; + + $db = \Config\Database::connect(); + + // Check if test already exists (avoid duplicates) + $existing = $db->table('patres') + ->where('OrderID', $internalOID) + ->where('TestSiteID', $testSiteID) + ->where('DelDate', null) + ->get() + ->getRow(); + + if ($existing) { + // Test already exists, skip + return; + } + + // Insert new test row + $ok = $db->table('patres')->insert([ + 'OrderID' => $internalOID, + 'TestSiteID' => $testSiteID, + 'TestSiteCode' => $testCode, + 'CreateDate' => date('Y-m-d H:i:s'), + ]); + + if ($ok === false) { + throw new \Exception('INSERT_TEST insert failed'); + } + } + + /** + * Execute ADD_COMMENT action - Add a comment to the order + */ + protected function executeAddComment(array $action, array $context): void + { + $order = $context['order'] ?? null; + if (!is_array($order) || empty($order['InternalOID'])) { + throw new \Exception('ADD_COMMENT requires context.order.InternalOID'); + } + + $internalOID = (int) $order['InternalOID']; + $comment = $action['comment'] ?? null; + + if (empty($comment)) { + throw new \Exception('ADD_COMMENT requires comment'); + } + + $db = \Config\Database::connect(); + + // Insert comment into ordercom table + $ok = $db->table('ordercom')->insert([ + 'InternalOID' => $internalOID, + 'Comment' => $comment, + 'CreateDate' => date('Y-m-d H:i:s'), + ]); + + if ($ok === false) { + throw new \Exception('ADD_COMMENT insert failed'); + } + } + + /** + * Execute TEST_DELETE action - soft delete a test from patres + */ + protected function executeDeleteTest(array $action, array $context): void + { + $order = $context['order'] ?? null; + if (!is_array($order) || empty($order['InternalOID'])) { + throw new \Exception('TEST_DELETE requires context.order.InternalOID'); + } + + $internalOID = (int) $order['InternalOID']; + $testCode = $action['testCode'] ?? null; + if (empty($testCode)) { + throw new \Exception('TEST_DELETE requires testCode'); + } + + $testDefSiteModel = new TestDefSiteModel(); + $testSite = $testDefSiteModel->where('TestSiteCode', $testCode) + ->where('EndDate', null) + ->first(); + + if (!$testSite || empty($testSite['TestSiteID'])) { + // Unknown test code: no-op + return; + } + + $testSiteID = (int) $testSite['TestSiteID']; + $db = \Config\Database::connect(); + + // Soft delete matching patres row(s) + $db->table('patres') + ->where('OrderID', $internalOID) + ->where('TestSiteID', $testSiteID) + ->where('DelDate', null) + ->update(['DelDate' => date('Y-m-d H:i:s')]); + } + + private function resolveTestSiteCode(int $testSiteID): ?string + { + try { + $testDefSiteModel = new TestDefSiteModel(); + $row = $testDefSiteModel->where('TestSiteID', $testSiteID)->where('EndDate', null)->first(); + return $row['TestSiteCode'] ?? null; + } catch (\Throwable $e) { + return null; + } + } + + private function resolveTestSiteIdByCode(string $testSiteCode): ?int + { + try { + $testDefSiteModel = new TestDefSiteModel(); + $row = $testDefSiteModel->where('TestSiteCode', $testSiteCode)->where('EndDate', null)->first(); + if (empty($row['TestSiteID'])) { + return null; + } + + return (int) $row['TestSiteID']; + } catch (\Throwable $e) { + return null; + } + } +} diff --git a/app/Services/RuleExpressionService.php b/app/Services/RuleExpressionService.php index 3d7c4bf..d520191 100755 --- a/app/Services/RuleExpressionService.php +++ b/app/Services/RuleExpressionService.php @@ -1,706 +1,706 @@ - */ - protected array $parsedCache = []; - - public function __construct() - { - $this->language = new ExpressionLanguage(); - } - - public function evaluate(string $expr, array $context = []) - { - $expr = trim($expr); - if ($expr === '') { - return null; - } - - $names = array_keys($context); - sort($names); - $cacheKey = md5($expr . '|' . implode(',', $names)); - - if (!isset($this->parsedCache[$cacheKey])) { - $this->parsedCache[$cacheKey] = $this->language->parse($expr, $names); - } - - return $this->language->evaluate($this->parsedCache[$cacheKey], $context); - } - - public function evaluateBoolean(?string $expr, array $context = []): bool - { - if ($expr === null || trim($expr) === '') { - return true; - } - - return (bool) $this->evaluate($expr, $context); - } - - /** - * Compile DSL expression to engine-compatible JSON structure. - * - * Supported DSL (canonical): - * - if(condition; then-actions; else-actions) - * - sex('F'|'M') -> patient["Sex"] == 'F' - * - priority('R'|'S'|'U') -> order["Priority"] == 'S' - * - age > 18 -> age > 18 - * - requested('CODE') -> requested('CODE') (resolved at runtime) - * - result_set('TestSiteCode', value) - * - result_set(value) (deprecated, uses current context TestSiteID) - * - test_insert('CODE') - * - test_delete('CODE') - * - comment_insert('text') - * - nothing - * - Multi-action: result_set(0.5):test_insert('CODE'):comment_insert('text') - * - * Backward compatible aliases: - * - set_result -> result_set - * - insert -> test_insert - * - add_comment -> comment_insert - * - * @param string $expr The raw DSL expression - * @return array The compiled structure with actions - * @throws \InvalidArgumentException If DSL is invalid - */ - public function compile(string $expr): array - { - $expr = trim($expr); - if ($expr === '') { - return []; - } - - $inner = $this->extractIfInner($expr); - if ($inner === null) { - // Allow callers to pass without if(...) wrapper - if (substr_count($expr, ';') >= 2) { - $inner = $expr; - } else { - throw new \InvalidArgumentException('Invalid DSL: expected "if(condition; then; else)" format'); - } - } - - $parts = $this->splitTopLevel($inner, ';', 3); - if (count($parts) !== 3) { - // Fallback: legacy ternary syntax - $ternary = $this->convertTopLevelTernaryToSemicolon($inner); - if ($ternary !== null) { - $parts = $this->splitTopLevel($ternary, ';', 3); - } - } - - if (count($parts) !== 3) { - throw new \InvalidArgumentException('Invalid DSL: expected exactly 3 parts "condition; then; else"'); - } - - $condition = trim($parts[0]); - $thenAction = trim($parts[1]); - $elseAction = trim($parts[2]); - - // Compile condition - $compiledCondition = $this->compileCondition($condition); - - // Compile actions (supports multi-action with : separator) - $thenActions = $this->compileMultiAction($thenAction); - $elseActions = $this->compileMultiAction($elseAction); - - // Build valueExpr for backward compatibility - $thenValueExpr = $this->buildValueExpr($thenActions); - $elseValueExpr = $this->buildValueExpr($elseActions); - - $valueExpr = "({$compiledCondition}) ? {$thenValueExpr} : {$elseValueExpr}"; - - return [ - 'conditionExpr' => $compiledCondition, - 'valueExpr' => $valueExpr, - 'then' => $thenActions, - 'else' => $elseActions, - ]; - } - - /** - * Compile DSL condition to ExpressionLanguage expression - * Supports: sex(), priority(), age, requested(), &&, ||, (), comparison operators - */ - private function compileCondition(string $condition): string - { - $condition = trim($condition); - if ($condition === '') { - return 'true'; - } - - $tokens = $this->tokenize($condition); - $out = ''; - $count = count($tokens); - - for ($i = 0; $i < $count; $i++) { - $t = $tokens[$i]; - $type = $t['type']; - $val = $t['value']; - - if ($type === 'op' && $val === '&&') { - $out .= ' and '; - continue; - } - if ($type === 'op' && $val === '||') { - $out .= ' or '; - continue; - } - - if ($type === 'ident') { - $lower = strtolower($val); - - // sex('M') - if ($lower === 'sex') { - $arg = $this->parseSingleStringArg($tokens, $i + 1); - if ($arg !== null) { - $out .= 'patient["Sex"] == "' . addslashes($arg['value']) . '"'; - $i = $arg['endIndex']; - continue; - } - } - - // priority('S') - if ($lower === 'priority') { - $arg = $this->parseSingleStringArg($tokens, $i + 1); - if ($arg !== null) { - $out .= 'order["Priority"] == "' . addslashes($arg['value']) . '"'; - $i = $arg['endIndex']; - continue; - } - } - - // requested('CODE') - if ($lower === 'requested') { - $arg = $this->parseSingleStringArg($tokens, $i + 1); - if ($arg !== null) { - $out .= 'requested("' . addslashes($arg['value']) . '")'; - $i = $arg['endIndex']; - continue; - } - } - - // age >= 18 - if ($lower === 'age') { - $op = $tokens[$i + 1] ?? null; - $num = $tokens[$i + 2] ?? null; - if (($op['type'] ?? '') === 'op' && in_array($op['value'], ['<', '>', '<=', '>=', '==', '!='], true) && ($num['type'] ?? '') === 'number') { - $out .= 'age ' . $op['value'] . ' ' . $num['value']; - $i += 2; - continue; - } - } - } - - $out .= $val; - } - - return trim($out); - } - - /** - * Compile multi-action string (separated by :) - * Returns array of action objects - */ - private function compileMultiAction(string $actionStr): array - { - $actionStr = trim($actionStr); - - // Split by : separator (top-level only) - $actions = []; - - $parts = $this->splitTopLevel($actionStr, ':'); - - foreach ($parts as $part) { - $action = $this->compileSingleAction(trim($part)); - if ($action !== null) { - $actions[] = $action; - } - } - - return $actions; - } - - /** - * Compile a single action - */ - private function compileSingleAction(string $action): ?array - { - $action = trim($action); - - // nothing - no operation - if (strcasecmp($action, 'nothing') === 0) { - return [ - 'type' => 'NO_OP', - 'value' => null, - ]; - } - - // result_set(value) or result_set('CODE', value) [aliases: set_result] - if (preg_match('/^(result_set|set_result)\s*\(\s*(.+)\s*\)$/is', $action, $m)) { - $args = $this->splitTopLevel($m[2], ',', 2); - if (empty($args)) { - throw new \InvalidArgumentException('result_set requires a value expression'); - } - - $testCode = null; - if (count($args) === 2) { - $testCode = $this->unquoteStringArgument($args[0]); - if ($testCode === null) { - throw new \InvalidArgumentException('result_set test code must be a quoted string'); - } - } - - $value = trim($args[count($args) - 1]); - - // Check if it's a number - if (is_numeric($value)) { - return [ - 'type' => 'RESULT_SET', - 'value' => strpos($value, '.') !== false ? (float) $value : (int) $value, - 'valueExpr' => $value, - 'testCode' => $testCode, - ]; - } - - // Check if it's a quoted string (single or double quotes) - if (preg_match('/^["\'](.+)["\']$/s', $value, $vm)) { - return [ - 'type' => 'RESULT_SET', - 'value' => $vm[1], - 'valueExpr' => '"' . addslashes($vm[1]) . '"', - 'testCode' => $testCode, - ]; - } - - // Complex expression - return [ - 'type' => 'RESULT_SET', - 'valueExpr' => $value, - 'testCode' => $testCode, - ]; - } - - // test_insert('CODE') [aliases: insert] - if (preg_match('/^(test_insert|insert)\s*\(\s*["\']([^"\']+)["\']\s*\)$/i', $action, $m)) { - return [ - 'type' => 'TEST_INSERT', - 'testCode' => $m[2], - ]; - } - - // test_delete('CODE') - if (preg_match('/^test_delete\s*\(\s*["\']([^"\']+)["\']\s*\)$/i', $action, $m)) { - return [ - 'type' => 'TEST_DELETE', - 'testCode' => $m[1], - ]; - } - - // comment_insert('text') [aliases: add_comment] - if (preg_match('/^(comment_insert|add_comment)\s*\(\s*["\'](.+?)["\']\s*\)$/is', $action, $m)) { - return [ - 'type' => 'COMMENT_INSERT', - 'comment' => $m[2], - ]; - } - - throw new \InvalidArgumentException('Unknown action: ' . $action); - } - - /** - * Build valueExpr string from array of actions (for backward compatibility) - */ - private function buildValueExpr(array $actions): string - { - if (empty($actions)) { - return 'null'; - } - - // Use first SET_RESULT action's value, or null - foreach ($actions as $action) { - if (($action['type'] ?? '') === 'RESULT_SET' || ($action['type'] ?? '') === 'SET_RESULT') { - if (isset($action['valueExpr'])) { - return $action['valueExpr']; - } - if (isset($action['value'])) { - if (is_string($action['value'])) { - return '"' . addslashes($action['value']) . '"'; - } - return json_encode($action['value']); - } - } - } - - return 'null'; - } - - /** - * Parse and split multi-rule expressions (comma-separated) - * Returns array of individual rule expressions - */ - public function parseMultiRule(string $expr): array - { - $expr = trim($expr); - if ($expr === '') { - return []; - } - - $rules = []; - $depth = 0; - $current = ''; - $len = strlen($expr); - - for ($i = 0; $i < $len; $i++) { - $char = $expr[$i]; - - if ($char === '(') { - $depth++; - } elseif ($char === ')') { - $depth--; - } - - if ($char === ',' && $depth === 0) { - $trimmed = trim($current); - if ($trimmed !== '') { - $rules[] = $trimmed; - } - $current = ''; - } else { - $current .= $char; - } - } - - // Add last rule - $trimmed = trim($current); - if ($trimmed !== '') { - $rules[] = $trimmed; - } - - return $rules; - } - - private function extractIfInner(string $expr): ?string - { - $expr = trim($expr); - if (!preg_match('/^if\s*\(/i', $expr)) { - return null; - } - - $pos = stripos($expr, '('); - if ($pos === false) { - return null; - } - - $start = $pos + 1; - $end = $this->findMatchingParen($expr, $pos); - if ($end === null) { - throw new \InvalidArgumentException('Invalid DSL: unbalanced parentheses in if(...)'); - } - - // Only allow trailing whitespace after the closing paren - if (trim(substr($expr, $end + 1)) !== '') { - throw new \InvalidArgumentException('Invalid DSL: unexpected trailing characters after if(...)'); - } - - return trim(substr($expr, $start, $end - $start)); - } - - private function findMatchingParen(string $s, int $openIndex): ?int - { - $len = strlen($s); - $depth = 0; - $inSingle = false; - $inDouble = false; - - for ($i = $openIndex; $i < $len; $i++) { - $ch = $s[$i]; - - if ($ch === '\\') { - $i++; - continue; - } - if (!$inDouble && $ch === "'") { - $inSingle = !$inSingle; - continue; - } - if (!$inSingle && $ch === '"') { - $inDouble = !$inDouble; - continue; - } - if ($inSingle || $inDouble) { - continue; - } - - if ($ch === '(') { - $depth++; - continue; - } - if ($ch === ')') { - $depth--; - if ($depth === 0) { - return $i; - } - } - } - return null; - } - - /** - * Split string by a delimiter only when not inside quotes/parentheses. - * If $limit is provided, splits into at most $limit parts. - */ - private function splitTopLevel(string $s, string $delimiter, ?int $limit = null): array - { - $s = (string) $s; - $len = strlen($s); - $depth = 0; - $inSingle = false; - $inDouble = false; - $parts = []; - $buf = ''; - - for ($i = 0; $i < $len; $i++) { - $ch = $s[$i]; - - if ($ch === '\\') { - $buf .= $ch; - if ($i + 1 < $len) { - $buf .= $s[$i + 1]; - $i++; - } - continue; - } - - if (!$inDouble && $ch === "'") { - $inSingle = !$inSingle; - $buf .= $ch; - continue; - } - if (!$inSingle && $ch === '"') { - $inDouble = !$inDouble; - $buf .= $ch; - continue; - } - - if (!$inSingle && !$inDouble) { - if ($ch === '(') { - $depth++; - } elseif ($ch === ')') { - $depth = max(0, $depth - 1); - } - - if ($depth === 0 && $ch === $delimiter) { - $parts[] = trim($buf); - $buf = ''; - if ($limit !== null && count($parts) >= ($limit - 1)) { - $buf .= substr($s, $i + 1); - $i = $len; - } - continue; - } - } - - $buf .= $ch; - } - - $parts[] = trim($buf); - $parts = array_values(array_filter($parts, static fn ($p) => $p !== '')); - return $parts; - } - - /** - * Convert a top-level ternary (condition ? then : else) into semicolon form. - */ - private function convertTopLevelTernaryToSemicolon(string $s): ?string - { - $len = strlen($s); - $depth = 0; - $inSingle = false; - $inDouble = false; - $qPos = null; - $cPos = null; - - for ($i = 0; $i < $len; $i++) { - $ch = $s[$i]; - if ($ch === '\\') { - $i++; - continue; - } - if (!$inDouble && $ch === "'") { - $inSingle = !$inSingle; - continue; - } - if (!$inSingle && $ch === '"') { - $inDouble = !$inDouble; - continue; - } - if ($inSingle || $inDouble) { - continue; - } - if ($ch === '(') { - $depth++; - continue; - } - if ($ch === ')') { - $depth = max(0, $depth - 1); - continue; - } - if ($depth === 0 && $ch === '?' && $qPos === null) { - $qPos = $i; - continue; - } - if ($depth === 0 && $ch === ':' && $qPos !== null) { - $cPos = $i; - break; - } - } - - if ($qPos === null || $cPos === null) { - return null; - } - - $cond = trim(substr($s, 0, $qPos)); - $then = trim(substr($s, $qPos + 1, $cPos - $qPos - 1)); - $else = trim(substr($s, $cPos + 1)); - - if ($cond === '' || $then === '' || $else === '') { - return null; - } - - return $cond . '; ' . $then . '; ' . $else; - } - - /** - * Tokenize a condition for lightweight transforms. - * Returns tokens with keys: type (ident|number|string|op|punct|other), value. - */ - private function tokenize(string $s): array - { - $len = strlen($s); - $tokens = []; - - for ($i = 0; $i < $len; $i++) { - $ch = $s[$i]; - - if (ctype_space($ch)) { - continue; - } - - $two = ($i + 1 < $len) ? $ch . $s[$i + 1] : ''; - if (in_array($two, ['&&', '||', '>=', '<=', '==', '!='], true)) { - $tokens[] = ['type' => 'op', 'value' => $two]; - $i++; - continue; - } - - if (in_array($ch, ['>', '<', '(', ')', '[', ']', ',', '+', '-', '*', '/', '%', '!', '.', ':', '?'], true)) { - $tokens[] = ['type' => ($ch === '>' || $ch === '<' || $ch === '!' ? 'op' : 'punct'), 'value' => $ch]; - continue; - } - - if ($ch === '"' || $ch === "'") { - $quote = $ch; - $buf = $quote; - $i++; - for (; $i < $len; $i++) { - $c = $s[$i]; - $buf .= $c; - if ($c === '\\' && $i + 1 < $len) { - $buf .= $s[$i + 1]; - $i++; - continue; - } - if ($c === $quote) { - break; - } - } - $tokens[] = ['type' => 'string', 'value' => $buf]; - continue; - } - - if (ctype_digit($ch)) { - $buf = $ch; - while ($i + 1 < $len && (ctype_digit($s[$i + 1]) || $s[$i + 1] === '.')) { - $buf .= $s[$i + 1]; - $i++; - } - $tokens[] = ['type' => 'number', 'value' => $buf]; - continue; - } - - if (ctype_alpha($ch) || $ch === '_') { - $buf = $ch; - while ($i + 1 < $len && (ctype_alnum($s[$i + 1]) || $s[$i + 1] === '_')) { - $buf .= $s[$i + 1]; - $i++; - } - $tokens[] = ['type' => 'ident', 'value' => $buf]; - continue; - } - - $tokens[] = ['type' => 'other', 'value' => $ch]; - } - - return $tokens; - } - - /** - * Parse a single quoted string argument from tokens starting at $start. - * Expects: '(' string ')'. Returns ['value' => string, 'endIndex' => int] or null. - */ - private function parseSingleStringArg(array $tokens, int $start): ?array - { - $t0 = $tokens[$start] ?? null; - $t1 = $tokens[$start + 1] ?? null; - $t2 = $tokens[$start + 2] ?? null; - if (($t0['value'] ?? null) !== '(') { - return null; - } - if (($t1['type'] ?? null) !== 'string') { - return null; - } - if (($t2['value'] ?? null) !== ')') { - return null; - } - - $raw = $t1['value']; - $quote = $raw[0] ?? ''; - if ($quote !== '"' && $quote !== "'") { - return null; - } - $val = substr($raw, 1, -1); - - return [ - 'value' => $val, - 'endIndex' => $start + 2, - ]; - } - - private function unquoteStringArgument(string $value): ?string - { - $value = trim($value); - if ($value === '') { - return null; - } - - $quote = $value[0]; - if ($quote !== '"' && $quote !== "'") { - return null; - } - - if (substr($value, -1) !== $quote) { - return null; - } - - $content = substr($value, 1, -1); - return stripcslashes($content); - } -} + */ + protected array $parsedCache = []; + + public function __construct() + { + $this->language = new ExpressionLanguage(); + } + + public function evaluate(string $expr, array $context = []) + { + $expr = trim($expr); + if ($expr === '') { + return null; + } + + $names = array_keys($context); + sort($names); + $cacheKey = md5($expr . '|' . implode(',', $names)); + + if (!isset($this->parsedCache[$cacheKey])) { + $this->parsedCache[$cacheKey] = $this->language->parse($expr, $names); + } + + return $this->language->evaluate($this->parsedCache[$cacheKey], $context); + } + + public function evaluateBoolean(?string $expr, array $context = []): bool + { + if ($expr === null || trim($expr) === '') { + return true; + } + + return (bool) $this->evaluate($expr, $context); + } + + /** + * Compile DSL expression to engine-compatible JSON structure. + * + * Supported DSL (canonical): + * - if(condition; then-actions; else-actions) + * - sex('F'|'M') -> patient["Sex"] == 'F' + * - priority('R'|'S'|'U') -> order["Priority"] == 'S' + * - age > 18 -> age > 18 + * - requested('CODE') -> requested('CODE') (resolved at runtime) + * - result_set('TestSiteCode', value) + * - result_set(value) (deprecated, uses current context TestSiteID) + * - test_insert('CODE') + * - test_delete('CODE') + * - comment_insert('text') + * - nothing + * - Multi-action: result_set(0.5):test_insert('CODE'):comment_insert('text') + * + * Backward compatible aliases: + * - set_result -> result_set + * - insert -> test_insert + * - add_comment -> comment_insert + * + * @param string $expr The raw DSL expression + * @return array The compiled structure with actions + * @throws \InvalidArgumentException If DSL is invalid + */ + public function compile(string $expr): array + { + $expr = trim($expr); + if ($expr === '') { + return []; + } + + $inner = $this->extractIfInner($expr); + if ($inner === null) { + // Allow callers to pass without if(...) wrapper + if (substr_count($expr, ';') >= 2) { + $inner = $expr; + } else { + throw new \InvalidArgumentException('Invalid DSL: expected "if(condition; then; else)" format'); + } + } + + $parts = $this->splitTopLevel($inner, ';', 3); + if (count($parts) !== 3) { + // Fallback: legacy ternary syntax + $ternary = $this->convertTopLevelTernaryToSemicolon($inner); + if ($ternary !== null) { + $parts = $this->splitTopLevel($ternary, ';', 3); + } + } + + if (count($parts) !== 3) { + throw new \InvalidArgumentException('Invalid DSL: expected exactly 3 parts "condition; then; else"'); + } + + $condition = trim($parts[0]); + $thenAction = trim($parts[1]); + $elseAction = trim($parts[2]); + + // Compile condition + $compiledCondition = $this->compileCondition($condition); + + // Compile actions (supports multi-action with : separator) + $thenActions = $this->compileMultiAction($thenAction); + $elseActions = $this->compileMultiAction($elseAction); + + // Build valueExpr for backward compatibility + $thenValueExpr = $this->buildValueExpr($thenActions); + $elseValueExpr = $this->buildValueExpr($elseActions); + + $valueExpr = "({$compiledCondition}) ? {$thenValueExpr} : {$elseValueExpr}"; + + return [ + 'conditionExpr' => $compiledCondition, + 'valueExpr' => $valueExpr, + 'then' => $thenActions, + 'else' => $elseActions, + ]; + } + + /** + * Compile DSL condition to ExpressionLanguage expression + * Supports: sex(), priority(), age, requested(), &&, ||, (), comparison operators + */ + private function compileCondition(string $condition): string + { + $condition = trim($condition); + if ($condition === '') { + return 'true'; + } + + $tokens = $this->tokenize($condition); + $out = ''; + $count = count($tokens); + + for ($i = 0; $i < $count; $i++) { + $t = $tokens[$i]; + $type = $t['type']; + $val = $t['value']; + + if ($type === 'op' && $val === '&&') { + $out .= ' and '; + continue; + } + if ($type === 'op' && $val === '||') { + $out .= ' or '; + continue; + } + + if ($type === 'ident') { + $lower = strtolower($val); + + // sex('M') + if ($lower === 'sex') { + $arg = $this->parseSingleStringArg($tokens, $i + 1); + if ($arg !== null) { + $out .= 'patient["Sex"] == "' . addslashes($arg['value']) . '"'; + $i = $arg['endIndex']; + continue; + } + } + + // priority('S') + if ($lower === 'priority') { + $arg = $this->parseSingleStringArg($tokens, $i + 1); + if ($arg !== null) { + $out .= 'order["Priority"] == "' . addslashes($arg['value']) . '"'; + $i = $arg['endIndex']; + continue; + } + } + + // requested('CODE') + if ($lower === 'requested') { + $arg = $this->parseSingleStringArg($tokens, $i + 1); + if ($arg !== null) { + $out .= 'requested("' . addslashes($arg['value']) . '")'; + $i = $arg['endIndex']; + continue; + } + } + + // age >= 18 + if ($lower === 'age') { + $op = $tokens[$i + 1] ?? null; + $num = $tokens[$i + 2] ?? null; + if (($op['type'] ?? '') === 'op' && in_array($op['value'], ['<', '>', '<=', '>=', '==', '!='], true) && ($num['type'] ?? '') === 'number') { + $out .= 'age ' . $op['value'] . ' ' . $num['value']; + $i += 2; + continue; + } + } + } + + $out .= $val; + } + + return trim($out); + } + + /** + * Compile multi-action string (separated by :) + * Returns array of action objects + */ + private function compileMultiAction(string $actionStr): array + { + $actionStr = trim($actionStr); + + // Split by : separator (top-level only) + $actions = []; + + $parts = $this->splitTopLevel($actionStr, ':'); + + foreach ($parts as $part) { + $action = $this->compileSingleAction(trim($part)); + if ($action !== null) { + $actions[] = $action; + } + } + + return $actions; + } + + /** + * Compile a single action + */ + private function compileSingleAction(string $action): ?array + { + $action = trim($action); + + // nothing - no operation + if (strcasecmp($action, 'nothing') === 0) { + return [ + 'type' => 'NO_OP', + 'value' => null, + ]; + } + + // result_set(value) or result_set('CODE', value) [aliases: set_result] + if (preg_match('/^(result_set|set_result)\s*\(\s*(.+)\s*\)$/is', $action, $m)) { + $args = $this->splitTopLevel($m[2], ',', 2); + if (empty($args)) { + throw new \InvalidArgumentException('result_set requires a value expression'); + } + + $testCode = null; + if (count($args) === 2) { + $testCode = $this->unquoteStringArgument($args[0]); + if ($testCode === null) { + throw new \InvalidArgumentException('result_set test code must be a quoted string'); + } + } + + $value = trim($args[count($args) - 1]); + + // Check if it's a number + if (is_numeric($value)) { + return [ + 'type' => 'RESULT_SET', + 'value' => strpos($value, '.') !== false ? (float) $value : (int) $value, + 'valueExpr' => $value, + 'testCode' => $testCode, + ]; + } + + // Check if it's a quoted string (single or double quotes) + if (preg_match('/^["\'](.+)["\']$/s', $value, $vm)) { + return [ + 'type' => 'RESULT_SET', + 'value' => $vm[1], + 'valueExpr' => '"' . addslashes($vm[1]) . '"', + 'testCode' => $testCode, + ]; + } + + // Complex expression + return [ + 'type' => 'RESULT_SET', + 'valueExpr' => $value, + 'testCode' => $testCode, + ]; + } + + // test_insert('CODE') [aliases: insert] + if (preg_match('/^(test_insert|insert)\s*\(\s*["\']([^"\']+)["\']\s*\)$/i', $action, $m)) { + return [ + 'type' => 'TEST_INSERT', + 'testCode' => $m[2], + ]; + } + + // test_delete('CODE') + if (preg_match('/^test_delete\s*\(\s*["\']([^"\']+)["\']\s*\)$/i', $action, $m)) { + return [ + 'type' => 'TEST_DELETE', + 'testCode' => $m[1], + ]; + } + + // comment_insert('text') [aliases: add_comment] + if (preg_match('/^(comment_insert|add_comment)\s*\(\s*["\'](.+?)["\']\s*\)$/is', $action, $m)) { + return [ + 'type' => 'COMMENT_INSERT', + 'comment' => $m[2], + ]; + } + + throw new \InvalidArgumentException('Unknown action: ' . $action); + } + + /** + * Build valueExpr string from array of actions (for backward compatibility) + */ + private function buildValueExpr(array $actions): string + { + if (empty($actions)) { + return 'null'; + } + + // Use first SET_RESULT action's value, or null + foreach ($actions as $action) { + if (($action['type'] ?? '') === 'RESULT_SET' || ($action['type'] ?? '') === 'SET_RESULT') { + if (isset($action['valueExpr'])) { + return $action['valueExpr']; + } + if (isset($action['value'])) { + if (is_string($action['value'])) { + return '"' . addslashes($action['value']) . '"'; + } + return json_encode($action['value']); + } + } + } + + return 'null'; + } + + /** + * Parse and split multi-rule expressions (comma-separated) + * Returns array of individual rule expressions + */ + public function parseMultiRule(string $expr): array + { + $expr = trim($expr); + if ($expr === '') { + return []; + } + + $rules = []; + $depth = 0; + $current = ''; + $len = strlen($expr); + + for ($i = 0; $i < $len; $i++) { + $char = $expr[$i]; + + if ($char === '(') { + $depth++; + } elseif ($char === ')') { + $depth--; + } + + if ($char === ',' && $depth === 0) { + $trimmed = trim($current); + if ($trimmed !== '') { + $rules[] = $trimmed; + } + $current = ''; + } else { + $current .= $char; + } + } + + // Add last rule + $trimmed = trim($current); + if ($trimmed !== '') { + $rules[] = $trimmed; + } + + return $rules; + } + + private function extractIfInner(string $expr): ?string + { + $expr = trim($expr); + if (!preg_match('/^if\s*\(/i', $expr)) { + return null; + } + + $pos = stripos($expr, '('); + if ($pos === false) { + return null; + } + + $start = $pos + 1; + $end = $this->findMatchingParen($expr, $pos); + if ($end === null) { + throw new \InvalidArgumentException('Invalid DSL: unbalanced parentheses in if(...)'); + } + + // Only allow trailing whitespace after the closing paren + if (trim(substr($expr, $end + 1)) !== '') { + throw new \InvalidArgumentException('Invalid DSL: unexpected trailing characters after if(...)'); + } + + return trim(substr($expr, $start, $end - $start)); + } + + private function findMatchingParen(string $s, int $openIndex): ?int + { + $len = strlen($s); + $depth = 0; + $inSingle = false; + $inDouble = false; + + for ($i = $openIndex; $i < $len; $i++) { + $ch = $s[$i]; + + if ($ch === '\\') { + $i++; + continue; + } + if (!$inDouble && $ch === "'") { + $inSingle = !$inSingle; + continue; + } + if (!$inSingle && $ch === '"') { + $inDouble = !$inDouble; + continue; + } + if ($inSingle || $inDouble) { + continue; + } + + if ($ch === '(') { + $depth++; + continue; + } + if ($ch === ')') { + $depth--; + if ($depth === 0) { + return $i; + } + } + } + return null; + } + + /** + * Split string by a delimiter only when not inside quotes/parentheses. + * If $limit is provided, splits into at most $limit parts. + */ + private function splitTopLevel(string $s, string $delimiter, ?int $limit = null): array + { + $s = (string) $s; + $len = strlen($s); + $depth = 0; + $inSingle = false; + $inDouble = false; + $parts = []; + $buf = ''; + + for ($i = 0; $i < $len; $i++) { + $ch = $s[$i]; + + if ($ch === '\\') { + $buf .= $ch; + if ($i + 1 < $len) { + $buf .= $s[$i + 1]; + $i++; + } + continue; + } + + if (!$inDouble && $ch === "'") { + $inSingle = !$inSingle; + $buf .= $ch; + continue; + } + if (!$inSingle && $ch === '"') { + $inDouble = !$inDouble; + $buf .= $ch; + continue; + } + + if (!$inSingle && !$inDouble) { + if ($ch === '(') { + $depth++; + } elseif ($ch === ')') { + $depth = max(0, $depth - 1); + } + + if ($depth === 0 && $ch === $delimiter) { + $parts[] = trim($buf); + $buf = ''; + if ($limit !== null && count($parts) >= ($limit - 1)) { + $buf .= substr($s, $i + 1); + $i = $len; + } + continue; + } + } + + $buf .= $ch; + } + + $parts[] = trim($buf); + $parts = array_values(array_filter($parts, static fn ($p) => $p !== '')); + return $parts; + } + + /** + * Convert a top-level ternary (condition ? then : else) into semicolon form. + */ + private function convertTopLevelTernaryToSemicolon(string $s): ?string + { + $len = strlen($s); + $depth = 0; + $inSingle = false; + $inDouble = false; + $qPos = null; + $cPos = null; + + for ($i = 0; $i < $len; $i++) { + $ch = $s[$i]; + if ($ch === '\\') { + $i++; + continue; + } + if (!$inDouble && $ch === "'") { + $inSingle = !$inSingle; + continue; + } + if (!$inSingle && $ch === '"') { + $inDouble = !$inDouble; + continue; + } + if ($inSingle || $inDouble) { + continue; + } + if ($ch === '(') { + $depth++; + continue; + } + if ($ch === ')') { + $depth = max(0, $depth - 1); + continue; + } + if ($depth === 0 && $ch === '?' && $qPos === null) { + $qPos = $i; + continue; + } + if ($depth === 0 && $ch === ':' && $qPos !== null) { + $cPos = $i; + break; + } + } + + if ($qPos === null || $cPos === null) { + return null; + } + + $cond = trim(substr($s, 0, $qPos)); + $then = trim(substr($s, $qPos + 1, $cPos - $qPos - 1)); + $else = trim(substr($s, $cPos + 1)); + + if ($cond === '' || $then === '' || $else === '') { + return null; + } + + return $cond . '; ' . $then . '; ' . $else; + } + + /** + * Tokenize a condition for lightweight transforms. + * Returns tokens with keys: type (ident|number|string|op|punct|other), value. + */ + private function tokenize(string $s): array + { + $len = strlen($s); + $tokens = []; + + for ($i = 0; $i < $len; $i++) { + $ch = $s[$i]; + + if (ctype_space($ch)) { + continue; + } + + $two = ($i + 1 < $len) ? $ch . $s[$i + 1] : ''; + if (in_array($two, ['&&', '||', '>=', '<=', '==', '!='], true)) { + $tokens[] = ['type' => 'op', 'value' => $two]; + $i++; + continue; + } + + if (in_array($ch, ['>', '<', '(', ')', '[', ']', ',', '+', '-', '*', '/', '%', '!', '.', ':', '?'], true)) { + $tokens[] = ['type' => ($ch === '>' || $ch === '<' || $ch === '!' ? 'op' : 'punct'), 'value' => $ch]; + continue; + } + + if ($ch === '"' || $ch === "'") { + $quote = $ch; + $buf = $quote; + $i++; + for (; $i < $len; $i++) { + $c = $s[$i]; + $buf .= $c; + if ($c === '\\' && $i + 1 < $len) { + $buf .= $s[$i + 1]; + $i++; + continue; + } + if ($c === $quote) { + break; + } + } + $tokens[] = ['type' => 'string', 'value' => $buf]; + continue; + } + + if (ctype_digit($ch)) { + $buf = $ch; + while ($i + 1 < $len && (ctype_digit($s[$i + 1]) || $s[$i + 1] === '.')) { + $buf .= $s[$i + 1]; + $i++; + } + $tokens[] = ['type' => 'number', 'value' => $buf]; + continue; + } + + if (ctype_alpha($ch) || $ch === '_') { + $buf = $ch; + while ($i + 1 < $len && (ctype_alnum($s[$i + 1]) || $s[$i + 1] === '_')) { + $buf .= $s[$i + 1]; + $i++; + } + $tokens[] = ['type' => 'ident', 'value' => $buf]; + continue; + } + + $tokens[] = ['type' => 'other', 'value' => $ch]; + } + + return $tokens; + } + + /** + * Parse a single quoted string argument from tokens starting at $start. + * Expects: '(' string ')'. Returns ['value' => string, 'endIndex' => int] or null. + */ + private function parseSingleStringArg(array $tokens, int $start): ?array + { + $t0 = $tokens[$start] ?? null; + $t1 = $tokens[$start + 1] ?? null; + $t2 = $tokens[$start + 2] ?? null; + if (($t0['value'] ?? null) !== '(') { + return null; + } + if (($t1['type'] ?? null) !== 'string') { + return null; + } + if (($t2['value'] ?? null) !== ')') { + return null; + } + + $raw = $t1['value']; + $quote = $raw[0] ?? ''; + if ($quote !== '"' && $quote !== "'") { + return null; + } + $val = substr($raw, 1, -1); + + return [ + 'value' => $val, + 'endIndex' => $start + 2, + ]; + } + + private function unquoteStringArgument(string $value): ?string + { + $value = trim($value); + if ($value === '') { + return null; + } + + $quote = $value[0]; + if ($quote !== '"' && $quote !== "'") { + return null; + } + + if (substr($value, -1) !== $quote) { + return null; + } + + $content = substr($value, 1, -1); + return stripcslashes($content); + } +} diff --git a/app/Traits/PatchValidationTrait.php b/app/Traits/PatchValidationTrait.php index 42b9dff..45810ea 100755 --- a/app/Traits/PatchValidationTrait.php +++ b/app/Traits/PatchValidationTrait.php @@ -1,20 +1,20 @@ -failValidationErrors("{$label} is required and must be a valid integer."); - return null; - } - return (int) $id; - } - - protected function requirePatchPayload(mixed $payload): ?array { - if (!is_array($payload) || empty($payload)) { - $this->failValidationErrors('No data provided for update.'); - return null; - } - return $payload; - } -} +failValidationErrors("{$label} is required and must be a valid integer."); + return null; + } + return (int) $id; + } + + protected function requirePatchPayload(mixed $payload): ?array { + if (!is_array($payload) || empty($payload)) { + $this->failValidationErrors('No data provided for update.'); + return null; + } + return $payload; + } +} diff --git a/composer.lock b/composer.lock index ddabbb6..e916e1a 100755 --- a/composer.lock +++ b/composer.lock @@ -88,12 +88,12 @@ "version": "v7.0.5", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", + "url": "https://github.com/googleapis/php-jwt.git", "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", "shasum": "" }, @@ -142,8 +142,8 @@ "php" ], "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v7.0.5" + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" }, "time": "2026-04-01T20:38:03+00:00" }, @@ -362,30 +362,34 @@ }, { "name": "symfony/cache", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "8abf3ccbeae9d3071b81a3ae7ee11b209f9e1e78" + "reference": "467464da294734b0fb17e853e5712abc8470f819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/8abf3ccbeae9d3071b81a3ae7ee11b209f9e1e78", - "reference": "8abf3ccbeae9d3071b81a3ae7ee11b209f9e1e78", + "url": "https://api.github.com/repos/symfony/cache/zipball/467464da294734b0fb17e853e5712abc8470f819", + "reference": "467464da294734b0fb17e853e5712abc8470f819", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "conflict": { - "doctrine/dbal": "<4.3", + "doctrine/dbal": "<3.6", "ext-redis": "<6.1", - "ext-relay": "<0.12.1" + "ext-relay": "<0.12.1", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" }, "provide": { "psr/cache-implementation": "2.0|3.0", @@ -394,16 +398,16 @@ }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/dbal": "^4.3", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^7.4|^8.0", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/filesystem": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -438,7 +442,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.0.8" + "source": "https://github.com/symfony/cache/tree/v7.4.8" }, "funding": [ { @@ -458,7 +462,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:18:51+00:00" + "time": "2026-03-30T15:15:47+00:00" }, { "name": "symfony/cache-contracts", @@ -760,25 +764,26 @@ }, { "name": "symfony/var-exporter", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "15776bb07a91b089037da89f8832fa41d5fa6ec6" + "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/15776bb07a91b089037da89f8832fa41d5fa6ec6", - "reference": "15776bb07a91b089037da89f8832fa41d5fa6ec6", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/398907e89a2a56fe426f7955c6fa943ec0c77225", + "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -816,7 +821,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v8.0.8" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.8" }, "funding": [ { @@ -836,7 +841,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-24T13:12:05+00:00" } ], "packages-dev": [ diff --git a/package-lock.json b/package-lock.json index c0e70f9..a2d1573 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ -{ - "name": "clqms01-be", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} +{ + "name": "clqms01-be", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index e24ed27..d3cb92a 100755 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -1,8809 +1,8809 @@ -openapi: 3.1.0 -info: - title: CLQMS - Clinical Laboratory Quality Management System API - description: | - API for Clinical Laboratory Quality Management System supporting patient management, - specimen tracking, test ordering, instrument integration, and laboratory operations. - - **IMPORTANT:** For OpenAPI tools (Swagger UI, Redoc, Postman, etc.), use the bundled file: - `api-docs.bundled.yaml` which contains all paths and schemas merged into one file. - - This file (api-docs.yaml) contains schema references and is meant for development. - The paths are defined in separate files in the `paths/` directory. - version: 1.0.0 - contact: - name: CLQMS API Support - license: - name: Proprietary -servers: - - url: http://localhost/clqms01/ - description: Local development server - - url: https://clqms01-api.services-summit.my.id/ - description: Production server -tags: - - name: Authentication - description: User authentication and session management - - name: Patient - description: Patient registration and management - - name: Patient Visit - description: Patient visit/encounter management - - name: Organization - description: Organization structure (accounts, sites, disciplines, departments, workstations) - - name: Location - description: Location management (rooms, wards, buildings) - - name: Equipment - description: Laboratory equipment and instrument management - - name: Specimen - description: Specimen and container management - - name: Test - description: Test definitions and test catalog - - name: Rule - description: Rule engine - rules can be linked to multiple tests via testrule mapping table - - name: Calculation - description: Lightweight calculator endpoint for retrieving computed values by code or name - - name: Order - description: Laboratory order management - - name: Result - description: Patient results reporting with auto-validation - - name: Report - description: Lab report generation (HTML view) - - name: Edge API - description: Instrument integration endpoints - - name: Contact - description: Contact management (doctors, practitioners, etc.) - - name: ValueSet - description: Value set definitions and items - - name: User - description: User management and administration - - name: Demo - description: Demo/test endpoints (no authentication) - - name: Audit - description: Audit log retrieval and filtering -paths: - /api/audit-logs: - get: - tags: - - Audit - summary: Retrieve audit log entries for a table - security: - - bearerAuth: [] - parameters: - - name: table - in: query - required: true - schema: - type: string - description: Table alias for the audit data (logpatient, logorder, logmaster, logsystem) - - name: rec_id - in: query - schema: - type: string - description: Primary record identifier (RecID) to filter audit rows - - name: event_id - in: query - schema: - type: string - description: Canonical EventID (case insensitive) - - name: activity_id - in: query - schema: - type: string - description: Canonical ActivityID (case insensitive) - - name: from - in: query - schema: - type: string - format: date-time - description: Lower bound for LogDate inclusive - - name: to - in: query - schema: - type: string - format: date-time - description: Upper bound for LogDate inclusive - - name: search - in: query - schema: - type: string - description: Search term that matches user, reason, field names, or values - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number - - name: perPage - in: query - schema: - type: integer - default: 20 - description: Items per page (max 100) - responses: - '200': - description: Audit log results - content: - application/json: - schema: - $ref: '#/components/schemas/AuditLogsEnvelope' - '400': - description: Validation failure (missing table or invalid filters) - content: - application/json: - schema: - $ref: '#/components/schemas/AuditLogsErrorResponse' - '500': - description: Internal error when retrieving audit logs - content: - application/json: - schema: - $ref: '#/components/schemas/AuditLogsErrorResponse' - /api/auth/login: - post: - tags: - - Authentication - summary: User login - description: Authenticate user and receive JWT token via HTTP-only cookie - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LoginRequest' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/LoginRequest' - responses: - '200': - description: Login successful - headers: - Set-Cookie: - description: JWT token in HTTP-only cookie - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/LoginResponse' - '400': - description: Missing username - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - description: Invalid credentials - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /api/auth/logout: - post: - tags: - - Authentication - summary: User logout - description: Clear JWT token cookie - security: - - bearerAuth: [] - responses: - '200': - description: Logout successful - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/auth/check: - get: - tags: - - Authentication - summary: Check authentication status - security: - - bearerAuth: [] - - cookieAuth: [] - responses: - '200': - description: Authenticated - content: - application/json: - schema: - type: object - properties: - authenticated: - type: boolean - user: - type: object - '401': - description: Not authenticated - /api/auth/register: - post: - tags: - - Authentication - summary: Register new user - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RegisterRequest' - responses: - '201': - description: User created - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/auth/change_pass: - post: - tags: - - Authentication - summary: Change password - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - current_password - - new_password - properties: - current_password: - type: string - format: password - new_password: - type: string - format: password - responses: - '200': - description: Password changed successfully - /v2/auth/login: - post: - tags: - - Authentication - summary: V2 User login - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LoginRequest' - responses: - '200': - description: Login successful - content: - application/json: - schema: - $ref: '#/components/schemas/LoginResponse' - /v2/auth/logout: - post: - tags: - - Authentication - summary: V2 User logout - responses: - '200': - description: Logout successful - /v2/auth/check: - get: - tags: - - Authentication - summary: V2 Check authentication - responses: - '200': - description: Auth check result - /v2/auth/register: - post: - tags: - - Authentication - summary: V2 Register new user - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RegisterRequest' - responses: - '201': - description: User created - /api/calc/testcode/{codeOrName}: - post: - tags: - - Calculation - summary: Evaluate a configured calculation by test code or name and return the raw result map. - security: [] - parameters: - - name: codeOrName - in: path - required: true - schema: - type: string - description: TestSiteCode or TestSiteName of the calculated test (case-insensitive). - requestBody: - required: true - content: - application/json: - schema: - type: object - description: Key-value pairs where keys match member tests used in the formula. - additionalProperties: - type: number - example: - TBIL: 5 - DBIL: 3 - responses: - '200': - description: Returns a single key/value pair with the canonical TestSiteCode or an empty object when the calculation is incomplete or missing. - content: - application/json: - schema: - type: object - examples: - success: - value: - 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: - - Contact - summary: List contacts - security: - - bearerAuth: [] - parameters: - - name: ContactName - in: query - schema: - type: string - description: Filter by contact name (searches in NameFirst and NameLast) - - name: Specialty - in: query - schema: - type: string - description: Filter by medical specialty code - responses: - '200': - description: List of contacts - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/Contact' - post: - tags: - - Contact - summary: Create new contact - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - NameFirst - properties: - NameFirst: - type: string - description: First name - NameLast: - type: string - description: Last name - Title: - type: string - description: Title (e.g., Dr, Mr, Mrs) - Initial: - type: string - description: Middle initial - Birthdate: - type: string - format: date-time - description: Date of birth - EmailAddress1: - type: string - format: email - description: Primary email address - EmailAddress2: - type: string - format: email - description: Secondary email address - Phone: - type: string - description: Primary phone number - MobilePhone1: - type: string - description: Primary mobile number - MobilePhone2: - type: string - description: Secondary mobile number - Specialty: - type: string - description: Medical specialty code - SubSpecialty: - type: string - description: Sub-specialty code - responses: - '201': - description: Contact created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '422': - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Contact - summary: Delete contact - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - ContactID - properties: - ContactID: - type: integer - description: Contact ID to delete - responses: - '200': - description: Contact deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/contact/{id}: - get: - tags: - - Contact - summary: Get contact by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Contact ID - responses: - '200': - description: Contact details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/Contact' - patch: - tags: - - Contact - summary: Update contact - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Contact ID to update - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - NameFirst - properties: - NameFirst: - type: string - description: First name - NameLast: - type: string - description: Last name - Title: - type: string - description: Title (e.g., Dr, Mr, Mrs) - Initial: - type: string - description: Middle initial - Birthdate: - type: string - format: date-time - description: Date of birth - EmailAddress1: - type: string - format: email - description: Primary email address - EmailAddress2: - type: string - format: email - description: Secondary email address - Phone: - type: string - description: Primary phone number - MobilePhone1: - type: string - description: Primary mobile number - MobilePhone2: - type: string - description: Secondary mobile number - Specialty: - type: string - description: Medical specialty code - SubSpecialty: - type: string - description: Sub-specialty code - Details: - description: | - Detail payload supports either a flat array of new rows (legacy format) - or an operations object with `created`, `edited`, and `deleted` arrays. - oneOf: - - $ref: '#/components/schemas/ContactDetailOperations' - - type: array - description: Legacy format for replacing details with new rows only - items: - $ref: '#/components/schemas/ContactDetail' - responses: - '201': - description: Contact updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '422': - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /api/demo/hello: - get: - tags: - - Demo - summary: Hello world endpoint - description: Simple test endpoint that returns a greeting message - responses: - '200': - description: Successful response - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Hello, World! - /api/demo/ping: - get: - tags: - - Demo - summary: Ping endpoint - description: Health check endpoint to verify API is running - responses: - '200': - description: API is running - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: pong - timestamp: - type: string - format: date-time - /api/edge/result: - post: - tags: - - Edge API - summary: Receive results from instrument (tiny-edge) - description: | - Receives instrument results and stores them in the edgeres table for processing. - This endpoint is typically called by the tiny-edge middleware connected to laboratory analyzers. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/EdgeResultRequest' - responses: - '201': - description: Result received and queued - content: - application/json: - schema: - $ref: '#/components/schemas/EdgeResultResponse' - '400': - description: Invalid JSON payload - /api/edge/order: - get: - tags: - - Edge API - summary: Fetch pending orders for instruments - description: Returns orders that need to be sent to laboratory instruments for testing - parameters: - - name: instrument_id - in: query - schema: - type: string - description: Filter by instrument - - name: status - in: query - schema: - type: string - enum: - - pending - - acknowledged - description: Filter by status - responses: - '200': - description: List of orders - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - type: array - items: - $ref: '#/components/schemas/EdgeOrder' - /api/edge/order/{orderId}/ack: - post: - tags: - - Edge API - summary: Acknowledge order delivery - description: Mark order as acknowledged by the instrument - parameters: - - name: orderId - in: path - required: true - schema: - type: integer - description: Edge order ID - responses: - '200': - description: Order acknowledged - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/edge/status: - post: - tags: - - Edge API - summary: Log instrument status update - description: Receive status updates from laboratory instruments - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - instrument_id - - status - properties: - instrument_id: - type: string - status: - type: string - enum: - - online - - offline - - error - - maintenance - message: - type: string - timestamp: - type: string - format: date-time - responses: - '200': - description: Status logged - /api/equipmentlist: - get: - tags: - - Equipment - summary: List equipment - description: Get list of equipment with optional filters - security: - - bearerAuth: [] - parameters: - - name: IEID - in: query - schema: - type: string - description: Filter by IEID - - name: InstrumentName - in: query - schema: - type: string - description: Filter by instrument name - - name: DepartmentID - in: query - schema: - type: integer - description: Filter by department ID - - name: WorkstationID - in: query - schema: - type: integer - description: Filter by workstation ID - - name: Enable - in: query - schema: - type: integer - enum: - - 0 - - 1 - description: Filter by enable status - responses: - '200': - description: List of equipment - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/EquipmentList' - post: - tags: - - Equipment - summary: Create equipment - description: Create a new equipment entry - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - IEID - - DepartmentID - - Enable - - EquipmentRole - properties: - IEID: - type: string - maxLength: 50 - DepartmentID: - type: integer - InstrumentID: - type: string - maxLength: 150 - InstrumentName: - type: string - maxLength: 150 - WorkstationID: - type: integer - Enable: - type: integer - enum: - - 0 - - 1 - EquipmentRole: - type: string - maxLength: 1 - responses: - '201': - description: Equipment created - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: integer - delete: - tags: - - Equipment - summary: Delete equipment - description: Soft delete an equipment entry - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - EID - properties: - EID: - type: integer - responses: - '200': - description: Equipment deleted - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - /api/equipmentlist/{id}: - get: - tags: - - Equipment - summary: Get equipment by ID - description: Get a single equipment entry by its EID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Equipment ID - responses: - '200': - description: Equipment details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/EquipmentList' - patch: - tags: - - Equipment - summary: Update equipment - description: Update an existing equipment entry - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Equipment ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - IEID: - type: string - maxLength: 50 - DepartmentID: - type: integer - InstrumentID: - type: string - maxLength: 150 - InstrumentName: - type: string - maxLength: 150 - WorkstationID: - type: integer - Enable: - type: integer - enum: - - 0 - - 1 - EquipmentRole: - type: string - maxLength: 1 - responses: - '200': - description: Equipment updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: integer - /api/location: - get: - tags: - - Location - summary: List locations - security: - - bearerAuth: [] - parameters: - - name: LocCode - in: query - schema: - type: string - description: Filter by location code - - name: LocName - in: query - schema: - type: string - description: Filter by location name (searches in LocFull) - responses: - '200': - description: List of locations - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/Location' - post: - tags: - - Location - summary: Create location - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - LocCode - - LocFull - properties: - SiteID: - type: integer - description: Reference to site - LocCode: - type: string - maxLength: 6 - description: Location code (short identifier) - Parent: - type: integer - nullable: true - description: Parent location ID for hierarchical locations - LocFull: - type: string - maxLength: 255 - description: Full location name - Description: - type: string - maxLength: 255 - description: Location description - LocType: - type: string - description: Location type code (e.g., ROOM, WARD, BUILDING) - responses: - '201': - description: Location created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '422': - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Location - summary: Delete location - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - LocationID - properties: - LocationID: - type: integer - description: Location ID to delete - responses: - '200': - description: Location deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/location/{id}: - get: - tags: - - Location - summary: Get location by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Location ID - responses: - '200': - description: Location details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/Location' - patch: - tags: - - Location - summary: Update location - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Location ID to update - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - SiteID: - type: integer - description: Reference to site - LocCode: - type: string - maxLength: 6 - description: Location code (short identifier) - Parent: - type: integer - nullable: true - description: Parent location ID for hierarchical locations - LocFull: - type: string - maxLength: 255 - description: Full location name - Description: - type: string - maxLength: 255 - description: Location description - LocType: - type: string - description: Location type code (e.g., ROOM, WARD, BUILDING) - responses: - '201': - description: Location updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '422': - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /api/ordertest: - get: - tags: - - Order - summary: List orders - security: - - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - - name: perPage - in: query - schema: - type: integer - - name: InternalPID - in: query - schema: - type: integer - description: Filter by internal patient ID - - name: OrderStatus - in: query - schema: - type: string - enum: - - ORD - - SCH - - ANA - - VER - - REV - - REP - description: | - ORD: Ordered - SCH: Scheduled - ANA: Analysis - VER: Verified - REV: Reviewed - REP: Reported - responses: - '200': - description: List of orders - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/OrderTestList' - post: - tags: - - Order - summary: Create order with specimens and tests - description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation. - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - InternalPID - - Tests - properties: - OrderID: - type: string - description: Optional custom order ID (auto-generated if not provided) - InternalPID: - type: integer - description: Patient internal ID - PatVisitID: - type: integer - description: Visit ID - SiteID: - type: integer - default: 1 - PlacerID: - type: string - Priority: - type: string - enum: - - R - - S - - U - default: R - description: | - R: Routine - S: Stat - U: Urgent - ReqApp: - type: string - description: Requesting application - Comment: - type: string - Tests: - type: array - items: - type: object - required: - - TestSiteID - properties: - TestSiteID: - type: integer - description: Test definition site ID - TestID: - type: integer - description: Alias for TestSiteID - responses: - '201': - description: Order created successfully with specimens and tests - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - $ref: '#/components/schemas/OrderTest' - '400': - description: Validation error - '500': - description: Server error - delete: - tags: - - Order - summary: Delete order - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - OrderID - properties: - OrderID: - type: string - responses: - '200': - description: Order deleted - /api/ordertest/status: - post: - tags: - - Order - summary: Update order status - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - OrderID - - OrderStatus - properties: - OrderID: - type: string - OrderStatus: - type: string - enum: - - ORD - - SCH - - ANA - - VER - - REV - - REP - description: | - ORD: Ordered - SCH: Scheduled - ANA: Analysis - VER: Verified - REV: Reviewed - REP: Reported - responses: - '200': - description: Order status updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/OrderTest' - /api/ordertest/{id}: - get: - tags: - - Order - summary: Get order by ID - description: Returns order details with associated specimens and tests - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - description: Order ID (e.g., 0025030300001) - responses: - '200': - description: Order details with specimens and tests - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/OrderTest' - patch: - tags: - - Order - summary: Update order - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - description: Order ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - Priority: - type: string - enum: - - R - - S - - U - OrderStatus: - type: string - enum: - - ORD - - SCH - - ANA - - VER - - REV - - REP - OrderingProvider: - type: string - DepartmentID: - type: integer - WorkstationID: - type: integer - responses: - '200': - description: Order updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/OrderTest' - /api/organization/account: - get: - tags: - - Organization - summary: List accounts - security: - - bearerAuth: [] - responses: - '200': - description: List of accounts - post: - tags: - - Organization - summary: Create account - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Account' - responses: - '201': - description: Account created - delete: - tags: - - Organization - summary: Delete account - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - AccountID - properties: - AccountID: - type: integer - responses: - '200': - description: Account deleted - /api/organization/account/{id}: - get: - tags: - - Organization - summary: Get account by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Account details - content: - application/json: - schema: - $ref: '#/components/schemas/Account' - patch: - tags: - - Organization - summary: Update account - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - AccountName: - type: string - Parent: - type: integer - responses: - '200': - description: Account updated - /api/organization/site: - get: - tags: - - Organization - summary: List sites - security: - - bearerAuth: [] - responses: - '200': - description: List of sites - post: - tags: - - Organization - summary: Create site - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Site' - responses: - '201': - description: Site created - delete: - tags: - - Organization - summary: Delete site - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - responses: - '200': - description: Site deleted - /api/organization/site/{id}: - get: - tags: - - Organization - summary: Get site by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Site details - patch: - tags: - - Organization - summary: Update site - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - SiteName: - type: string - SiteCode: - type: string - AccountID: - type: integer - responses: - '200': - description: Site updated - /api/organization/discipline: - get: - tags: - - Organization - summary: List disciplines - security: - - bearerAuth: [] - responses: - '200': - description: List of disciplines - post: - tags: - - Organization - summary: Create discipline - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Discipline' - responses: - '201': - description: Discipline created - delete: - tags: - - Organization - summary: Delete discipline - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - responses: - '200': - description: Discipline deleted - /api/organization/discipline/{id}: - get: - tags: - - Organization - summary: Get discipline by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Discipline details - patch: - tags: - - Organization - summary: Update discipline - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - DisciplineName: - type: string - DisciplineCode: - type: string - SeqScr: - type: integer - description: Display order on screen - SeqRpt: - type: integer - description: Display order in reports - responses: - '200': - description: Discipline updated - /api/organization/department: - get: - tags: - - Organization - summary: List departments - security: - - bearerAuth: [] - responses: - '200': - description: List of departments - post: - tags: - - Organization - summary: Create department - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Department' - responses: - '201': - description: Department created - delete: - tags: - - Organization - summary: Delete department - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - responses: - '200': - description: Department deleted - /api/organization/department/{id}: - get: - tags: - - Organization - summary: Get department by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Department details - patch: - tags: - - Organization - summary: Update department - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - DeptName: - type: string - DeptCode: - type: string - SiteID: - type: integer - responses: - '200': - description: Department updated - /api/organization/workstation: - get: - tags: - - Organization - summary: List workstations - security: - - bearerAuth: [] - responses: - '200': - description: List of workstations - post: - tags: - - Organization - summary: Create workstation - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Workstation' - responses: - '201': - description: Workstation created - delete: - tags: - - Organization - summary: Delete workstation - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - responses: - '200': - description: Workstation deleted - /api/organization/workstation/{id}: - get: - tags: - - Organization - summary: Get workstation by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Workstation details - patch: - tags: - - Organization - summary: Update workstation - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - WorkstationName: - type: string - WorkstationCode: - type: string - SiteID: - type: integer - DepartmentID: - type: integer - responses: - '200': - description: Workstation updated - /api/organization/hostapp: - get: - tags: - - Organization - summary: List host applications - security: - - bearerAuth: [] - parameters: - - name: HostAppID - in: query - schema: - type: string - - name: HostAppName - in: query - schema: - type: string - responses: - '200': - description: List of host applications - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/HostApp' - post: - tags: - - Organization - summary: Create host application - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/HostApp' - responses: - '201': - description: Host application created - delete: - tags: - - Organization - summary: Delete host application (soft delete) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - responses: - '200': - description: Host application deleted - /api/organization/hostapp/{id}: - get: - tags: - - Organization - summary: Get host application by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - responses: - '200': - description: Host application details - content: - application/json: - schema: - $ref: '#/components/schemas/HostApp' - patch: - tags: - - Organization - summary: Update host application - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - HostAppName: - type: string - SiteID: - type: integer - responses: - '200': - description: Host application updated - /api/organization/hostcompara: - get: - tags: - - Organization - summary: List host communication parameters - security: - - bearerAuth: [] - parameters: - - name: HostAppID - in: query - schema: - type: string - - name: HostIP - in: query - schema: - type: string - responses: - '200': - description: List of host communication parameters - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/HostComPara' - post: - tags: - - Organization - summary: Create host communication parameters - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/HostComPara' - responses: - '201': - description: Host communication parameters created - delete: - tags: - - Organization - summary: Delete host communication parameters (soft delete) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - responses: - '200': - description: Host communication parameters deleted - /api/organization/hostcompara/{id}: - get: - tags: - - Organization - summary: Get host communication parameters by HostAppID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - responses: - '200': - description: Host communication parameters details - content: - application/json: - schema: - $ref: '#/components/schemas/HostComPara' - patch: - tags: - - Organization - summary: Update host communication parameters - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - HostIP: - type: string - HostPort: - type: string - HostPwd: - type: string - responses: - '200': - description: Host communication parameters updated - /api/organization/codingsys: - get: - tags: - - Organization - summary: List coding systems - security: - - bearerAuth: [] - parameters: - - name: CodingSysAbb - in: query - schema: - type: string - - name: FullText - in: query - schema: - type: string - responses: - '200': - description: List of coding systems - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/CodingSys' - post: - tags: - - Organization - summary: Create coding system - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CodingSys' - responses: - '201': - description: Coding system created - delete: - tags: - - Organization - summary: Delete coding system (soft delete) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - CodingSysID - properties: - CodingSysID: - type: integer - responses: - '200': - description: Coding system deleted - /api/organization/codingsys/{id}: - get: - tags: - - Organization - summary: Get coding system by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Coding system details - content: - application/json: - schema: - $ref: '#/components/schemas/CodingSys' - patch: - tags: - - Organization - summary: Update coding system - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - CodingSysAbb: - type: string - FullText: - type: string - Description: - type: string - responses: - '200': - description: Coding system updated - /api/patvisit: - get: - tags: - - Patient Visit - summary: List patient visits - security: - - bearerAuth: [] - parameters: - - name: InternalPID - in: query - schema: - type: integer - description: Filter by internal patient ID (exact match) - - name: PVID - in: query - schema: - type: string - description: Filter by visit ID (partial match) - - name: PatientID - in: query - schema: - type: string - description: Filter by patient ID (partial match) - - name: PatientName - in: query - schema: - type: string - description: Search by patient name (searches in both first and last name) - - name: CreateDateFrom - in: query - schema: - type: string - format: date-time - description: Filter visits created on or after this date - - name: CreateDateTo - in: query - schema: - type: string - format: date-time - description: Filter visits created on or before this date - - name: page - in: query - schema: - type: integer - - name: perPage - in: query - schema: - type: integer - responses: - '200': - description: List of patient visits - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/PatientVisit' - total: - type: integer - description: Total number of records - page: - type: integer - description: Current page number - per_page: - type: integer - description: Number of records per page - post: - tags: - - Patient Visit - summary: Create patient visit - description: | - Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided. - Can optionally include PatDiag (diagnosis) and PatVisitADT (ADT information). - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - InternalPID - properties: - PVID: - type: string - description: Visit ID (auto-generated with DV prefix if not provided) - InternalPID: - type: integer - description: Patient ID (required) - EpisodeID: - type: string - description: Episode identifier - SiteID: - type: integer - description: Site reference - PatDiag: - type: object - description: Optional diagnosis information - properties: - DiagCode: - type: string - Diagnosis: - type: string - PatVisitADT: - type: object - description: Optional ADT information - properties: - ADTCode: - type: string - enum: - - A01 - - A02 - - A03 - - A04 - - A08 - LocationID: - type: integer - AttDoc: - type: integer - RefDoc: - type: integer - AdmDoc: - type: integer - CnsDoc: - type: integer - responses: - '201': - description: Visit created successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: object - properties: - PVID: - type: string - InternalPVID: - type: integer - delete: - tags: - - Patient Visit - summary: Delete patient visit - security: - - bearerAuth: [] - responses: - '200': - description: Visit deleted successfully - /api/patvisit/{id}: - get: - tags: - - Patient Visit - summary: Get visit by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - description: PVID (visit identifier like DV00001) - responses: - '200': - description: Visit details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/PatientVisit' - patch: - tags: - - Patient Visit - summary: Update patient visit - description: | - Updates an existing patient visit. InternalPVID is required. - Can update main visit data, PatDiag, and add new PatVisitADT records. - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Internal visit ID (InternalPVID) - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - PVID: - type: string - InternalPID: - type: integer - EpisodeID: - type: string - SiteID: - type: integer - PatDiag: - type: object - description: Diagnosis information (will update if exists) - properties: - DiagCode: - type: string - Diagnosis: - type: string - PatVisitADT: - type: array - description: Array of ADT records to add (new records only) - items: - type: object - properties: - ADTCode: - type: string - enum: - - A01 - - A02 - - A03 - - A04 - - A08 - LocationID: - type: integer - AttDoc: - type: integer - RefDoc: - type: integer - AdmDoc: - type: integer - CnsDoc: - type: integer - sequence: - type: integer - description: Used for ordering multiple ADT records - responses: - '200': - description: Visit updated successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: object - properties: - PVID: - type: string - InternalPVID: - type: integer - /api/patvisit/patient/{patientId}: - get: - tags: - - Patient Visit - summary: Get visits by patient ID - security: - - bearerAuth: [] - parameters: - - name: patientId - in: path - required: true - schema: - type: integer - description: Internal Patient ID (InternalPID) - responses: - '200': - description: Patient visits list - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - type: array - items: - $ref: '#/components/schemas/PatientVisit' - /api/patvisitadt: - post: - tags: - - Patient Visit - summary: Create ADT record - description: Create a new Admission/Discharge/Transfer record - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PatVisitADT' - responses: - '201': - description: ADT record created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - delete: - tags: - - Patient Visit - summary: Delete ADT visit (soft delete) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - PVADTID - properties: - PVADTID: - type: integer - description: ADT record ID to delete - responses: - '200': - description: ADT visit deleted successfully - /api/patvisitadt/visit/{visitId}: - get: - tags: - - Patient Visit - summary: Get ADT history by visit ID - description: Retrieve the complete Admission/Discharge/Transfer history for a visit, including all locations and doctors - security: - - bearerAuth: [] - parameters: - - name: visitId - in: path - required: true - schema: - type: integer - description: Internal Visit ID (InternalPVID) - responses: - '200': - description: ADT history retrieved successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: ADT history retrieved - data: - type: array - items: - type: object - properties: - PVADTID: - type: integer - InternalPVID: - type: integer - ADTCode: - type: string - enum: - - A01 - - A02 - - A03 - - A04 - - A08 - LocationID: - type: integer - LocationName: - type: string - AttDoc: - type: integer - AttDocFirstName: - type: string - AttDocLastName: - type: string - RefDoc: - type: integer - RefDocFirstName: - type: string - RefDocLastName: - type: string - AdmDoc: - type: integer - AdmDocFirstName: - type: string - AdmDocLastName: - type: string - CnsDoc: - type: integer - CnsDocFirstName: - type: string - CnsDocLastName: - type: string - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - /api/patvisitadt/{id}: - get: - tags: - - Patient Visit - summary: Get ADT record by ID - description: Retrieve a single ADT record by its ID, including location and doctor details - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: ADT record ID (PVADTID) - responses: - '200': - description: ADT record retrieved successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: ADT record retrieved - data: - type: object - properties: - PVADTID: - type: integer - InternalPVID: - type: integer - ADTCode: - type: string - enum: - - A01 - - A02 - - A03 - - A04 - - A08 - LocationID: - type: integer - LocationName: - type: string - AttDoc: - type: integer - AttDocFirstName: - type: string - AttDocLastName: - type: string - RefDoc: - type: integer - RefDocFirstName: - type: string - RefDocLastName: - type: string - AdmDoc: - type: integer - AdmDocFirstName: - type: string - AdmDocLastName: - type: string - CnsDoc: - type: integer - CnsDocFirstName: - type: string - CnsDocLastName: - type: string - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - patch: - tags: - - Patient Visit - summary: Update ADT record - description: Update an existing ADT record - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: ADT record ID (PVADTID) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PatVisitADT' - responses: - '200': - description: ADT record updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/patient: - get: - tags: - - Patient - summary: List patients - security: - - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - default: 1 - - name: perPage - in: query - schema: - type: integer - default: 20 - - name: InternalPID - in: query - schema: - type: integer - description: Filter by internal patient ID - - name: PatientID - in: query - schema: - type: string - description: Filter by patient ID - - name: Name - in: query - schema: - type: string - description: Search by patient name - - name: Birthdate - in: query - schema: - type: string - format: date - description: Filter by birthdate (YYYY-MM-DD) - responses: - '200': - description: List of patients - content: - application/json: - schema: - $ref: '#/components/schemas/PatientListResponse' - post: - tags: - - Patient - summary: Create new patient - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Patient' - responses: - '201': - description: Patient created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '422': - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Patient - summary: Delete patient (soft delete) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - InternalPID - properties: - InternalPID: - type: integer - description: Internal patient record ID - responses: - '200': - description: Patient deleted successfully - /api/patient/check: - get: - tags: - - Patient - summary: Check if patient exists - security: - - bearerAuth: [] - parameters: - - name: PatientID - in: query - schema: - type: string - description: Patient ID to check - - name: EmailAddress - in: query - schema: - type: string - format: email - description: Email address to check - - name: Phone - in: query - schema: - type: string - description: Phone number to check - responses: - '200': - description: Patient check result - content: - application/json: - schema: - type: object - properties: - exists: - type: boolean - data: - $ref: '#/components/schemas/Patient' - /api/patient/{id}: - get: - tags: - - Patient - summary: Get patient by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Internal patient record ID - responses: - '200': - description: Patient details - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - $ref: '#/components/schemas/Patient' - patch: - tags: - - Patient - summary: Partially update patient - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Internal patient record ID - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PatientPatch' - responses: - '200': - description: Patient updated successfully - '400': - description: Validation error - '404': - description: Patient not found - /api/report/{orderID}: - get: - tags: - - Report - summary: Generate lab report - description: Generate an HTML lab report for a specific order. Returns HTML content that can be viewed in browser or printed to PDF. - security: - - bearerAuth: [] - parameters: - - name: orderID - in: path - required: true - schema: - type: integer - description: Internal Order ID - responses: - '200': - description: HTML lab report - content: - text/html: - schema: - type: string - description: HTML content of the lab report - '404': - description: Order or patient not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Failed to generate report - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /api/result: - get: - tags: - - Result - summary: List results - description: Retrieve patient test results with optional filters by order or patient - security: - - bearerAuth: [] - parameters: - - name: order_id - in: query - schema: - type: integer - description: Filter by internal order ID - - name: patient_id - in: query - schema: - type: integer - description: Filter by internal patient ID (returns cumulative results) - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number for pagination - - name: per_page - in: query - schema: - type: integer - default: 20 - description: Number of results per page - responses: - '200': - description: List of results - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: array - items: - type: object - properties: - ResultID: - type: integer - OrderID: - type: integer - TestSiteID: - type: integer - TestSiteCode: - type: string - Result: - type: string - nullable: true - ResultDateTime: - type: string - format: date-time - RefNumID: - type: integer - nullable: true - RefTxtID: - type: integer - nullable: true - CreateDate: - type: string - format: date-time - TestSiteName: - type: string - nullable: true - Unit1: - type: string - nullable: true - Unit2: - type: string - nullable: true - Low: - type: number - nullable: true - High: - type: number - nullable: true - LowSign: - type: string - nullable: true - HighSign: - type: string - nullable: true - RefDisplay: - type: string - nullable: true - /api/result/{id}: - get: - tags: - - Result - summary: Get result by ID - description: Retrieve a specific result entry with all related data - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Result ID - responses: - '200': - description: Result details - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: object - properties: - ResultID: - type: integer - SiteID: - type: integer - OrderID: - type: integer - InternalSID: - type: integer - SID: - type: string - SampleID: - type: string - TestSiteID: - type: integer - TestSiteCode: - type: string - AspCnt: - type: integer - Result: - type: string - nullable: true - SampleType: - type: string - nullable: true - ResultDateTime: - type: string - format: date-time - WorkstationID: - type: integer - nullable: true - EquipmentID: - type: integer - nullable: true - RefNumID: - type: integer - nullable: true - RefTxtID: - type: integer - nullable: true - CreateDate: - type: string - format: date-time - TestSiteName: - type: string - nullable: true - Unit1: - type: string - nullable: true - Unit2: - type: string - nullable: true - Low: - type: number - nullable: true - High: - type: number - nullable: true - LowSign: - type: string - nullable: true - HighSign: - type: string - nullable: true - RefDisplay: - type: string - nullable: true - OrderNumber: - type: string - nullable: true - InternalPID: - type: integer - '404': - description: Result not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - patch: - tags: - - Result - summary: Update result - description: Update a result value with automatic validation against reference ranges. Returns calculated flag (L/H) in response but does not store it. - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Result ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - Result: - type: string - description: The result value - RefNumID: - type: integer - description: Reference range ID to validate against - SampleType: - type: string - nullable: true - WorkstationID: - type: integer - nullable: true - EquipmentID: - type: integer - nullable: true - responses: - '200': - description: Result updated successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: object - properties: - result: - type: object - flag: - type: string - nullable: true - enum: - - L - - H - description: Calculated flag - L for Low, H for High, null for normal - '400': - description: Validation failed - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Result not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Result - summary: Delete result - description: Soft delete a result entry by setting DelDate - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Result ID - responses: - '200': - description: Result deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '404': - description: Result not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /api/rule: - get: - tags: - - Rule - summary: List rules - security: - - bearerAuth: [] - parameters: - - name: EventCode - in: query - schema: - type: string - description: Filter by event code - - name: TestSiteID - in: query - schema: - type: integer - description: Filter by TestSiteID (returns rules linked to this test). Rules are only returned when attached to tests. - - name: search - in: query - schema: - type: string - description: Search by rule code or name - responses: - '200': - description: List of rules - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/RuleDef' - post: - tags: - - Rule - summary: Create rule - description: | - Create a new rule. Rules must be linked to at least one test via TestSiteIDs. - A single rule can be linked to multiple tests. Rules are active only when attached to tests. - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - RuleCode: - type: string - example: AUTO_SET_RESULT - RuleName: - type: string - example: Automatically Set Result - Description: - type: string - EventCode: - type: string - example: test_created - TestSiteIDs: - type: array - items: - type: integer - description: Array of TestSiteIDs to link this rule to (required) - example: - - 1 - - 2 - - 3 - ConditionExpr: - type: string - nullable: true - description: Raw DSL expression. Compile it first and persist the compiled JSON in ConditionExprCompiled. - example: if(sex('M'); result_set(0.5); result_set(0.6)) - ConditionExprCompiled: - type: string - nullable: true - description: Compiled JSON payload from POST /api/rule/compile - required: - - RuleCode - - RuleName - - EventCode - - TestSiteIDs - responses: - '201': - description: Rule created - /api/rule/{id}: - get: - tags: - - Rule - summary: Get rule with linked tests - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: RuleID - responses: - '200': - description: Rule details with linked test sites - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/RuleWithDetails' - '404': - description: Rule not found - patch: - tags: - - Rule - summary: Update rule - description: | - Update a rule. TestSiteIDs can be provided to update which tests the rule is linked to. - Tests not in the new list will be unlinked, and new tests will be linked. - Rules are active only when attached to tests. - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: RuleID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - RuleCode: - type: string - RuleName: - type: string - Description: - type: string - EventCode: - type: string - TestSiteIDs: - type: array - items: - type: integer - description: Array of TestSiteIDs to link this rule to - ConditionExpr: - type: string - nullable: true - responses: - '200': - description: Rule updated - '404': - description: Rule not found - delete: - tags: - - Rule - summary: Soft delete rule - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: RuleID - responses: - '200': - description: Rule deleted - '404': - description: Rule not found - /api/rule/validate: - post: - tags: - - Rule - summary: Validate/evaluate an expression - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - expr: - type: string - context: - type: object - additionalProperties: true - required: - - expr - responses: - '200': - description: Validation result - /api/rule/compile: - post: - tags: - - Rule - summary: Compile DSL expression to engine-compatible structure - description: | - Compile a DSL expression to the engine-compatible JSON structure. - Frontend calls this when user clicks "Compile" button. - Returns compiled structure that can be saved to ConditionExprCompiled field. - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - expr: - type: string - description: Raw DSL expression - example: if(sex('M'); result_set(0.5); result_set(0.6)) - required: - - expr - responses: - '200': - description: Compilation successful - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - data: - type: object - properties: - raw: - type: string - description: Original DSL expression - compiled: - type: object - description: Parsed structure with conditionExpr, valueExpr, then, else - conditionExprCompiled: - type: string - description: JSON string to save to ConditionExprCompiled field - '400': - description: Compilation failed (invalid syntax) - /api/specimen: - get: - tags: - - Specimen - summary: List specimens - security: - - bearerAuth: [] - responses: - '200': - description: List of specimens - post: - tags: - - Specimen - summary: Create specimen - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Specimen' - responses: - '201': - description: Specimen created - /api/specimen/{id}: - get: - tags: - - Specimen - summary: Get specimen by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Specimen details - patch: - tags: - - Specimen - summary: Update specimen - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Specimen ID (SID) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Specimen' - responses: - '200': - description: Specimen updated - delete: - tags: - - Specimen - summary: Delete specimen (soft delete) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Specimen ID (SID) - responses: - '200': - description: Specimen deleted successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Specimen deleted successfully - data: - type: object - properties: - SID: - type: integer - '404': - description: Specimen not found - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: failed - message: - type: string - example: Specimen not found - data: - type: null - '500': - description: Server error - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: failed - message: - type: string - example: Failed to delete specimen - data: - type: null - /api/specimen/container: - get: - tags: - - Specimen - summary: List container definitions - security: - - bearerAuth: [] - responses: - '200': - description: List of container definitions - post: - tags: - - Specimen - summary: Create container definition - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ContainerDef' - responses: - '201': - description: Container definition created - /api/specimen/container/{id}: - get: - tags: - - Specimen - summary: Get container definition by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Container definition details - patch: - tags: - - Specimen - summary: Update container definition - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Container definition ID (ConDefID) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ContainerDef' - responses: - '200': - description: Container definition updated - /api/specimen/containerdef: - get: - tags: - - Specimen - summary: List container definitions (alias) - security: - - bearerAuth: [] - responses: - '200': - description: List of container definitions - post: - tags: - - Specimen - summary: Create container definition (alias) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ContainerDef' - responses: - '201': - description: Container definition created - /api/specimen/containerdef/{id}: - patch: - tags: - - Specimen - summary: Update container definition (alias) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Container definition ID (ConDefID) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ContainerDef' - responses: - '200': - description: Container definition updated - /api/specimen/prep: - get: - tags: - - Specimen - summary: List specimen preparations - security: - - bearerAuth: [] - responses: - '200': - description: List of specimen preparations - post: - tags: - - Specimen - summary: Create specimen preparation - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SpecimenPrep' - responses: - '201': - description: Specimen preparation created - /api/specimen/prep/{id}: - get: - tags: - - Specimen - summary: Get specimen preparation by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Specimen preparation details - patch: - tags: - - Specimen - summary: Update specimen preparation - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Specimen preparation ID (SpcPrpID) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SpecimenPrep' - responses: - '200': - description: Specimen preparation updated - /api/specimen/status: - get: - tags: - - Specimen - summary: List specimen statuses - security: - - bearerAuth: [] - responses: - '200': - description: List of specimen statuses - post: - tags: - - Specimen - summary: Create specimen status - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SpecimenStatus' - responses: - '201': - description: Specimen status created - /api/specimen/status/{id}: - get: - tags: - - Specimen - summary: Get specimen status by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Specimen status details - patch: - tags: - - Specimen - summary: Update specimen status - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Specimen status ID (SpcStaID) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SpecimenStatus' - responses: - '200': - description: Specimen status updated - /api/specimen/collection: - get: - tags: - - Specimen - summary: List specimen collection methods - security: - - bearerAuth: [] - responses: - '200': - description: List of collection methods - post: - tags: - - Specimen - summary: Create specimen collection method - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SpecimenCollection' - responses: - '201': - description: Collection method created - /api/specimen/collection/{id}: - get: - tags: - - Specimen - summary: Get specimen collection method by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Collection method details - patch: - tags: - - Specimen - summary: Update specimen collection method - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Specimen collection ID (SpcColID) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SpecimenCollection' - responses: - '200': - description: Collection method updated - /api/test/testmap: - get: - tags: - - Test - summary: List all test mappings - security: - - bearerAuth: [] - parameters: - - name: host - in: query - required: false - schema: - type: string - description: Filter by host name, type, or ID - - name: client - in: query - required: false - schema: - type: string - description: Filter by client name, type, or ID - responses: - '200': - description: List of test mappings - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: array - items: - type: object - properties: - TestMapID: - type: integer - HostType: - type: string - HostID: - type: string - HostName: - type: string - ClientType: - type: string - ClientID: - type: string - ClientName: - type: string - post: - tags: - - Test - summary: Create test mapping (header only) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - HostType: - type: string - description: Host type code - HostID: - type: string - description: Host identifier - ClientType: - type: string - description: Client type code - ClientID: - type: string - description: Client identifier - details: - type: array - description: Optional detail records to create alongside the header - items: - type: object - properties: - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - responses: - '201': - description: Test mapping created - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: integer - description: Created TestMapID - delete: - tags: - - Test - summary: Soft delete test mapping (cascades to details) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapID: - type: integer - description: Test Map ID to delete (required) - required: - - TestMapID - responses: - '200': - description: Test mapping deleted successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: integer - description: Deleted TestMapID - '404': - description: Test mapping not found or already deleted - /api/test/testmap/{id}: - get: - tags: - - Test - summary: Get test mapping by ID with details - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Test Map ID - responses: - '200': - description: Test mapping details with nested detail records - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/TestMap' - '404': - description: Test mapping not found - patch: - tags: - - Test - summary: Update test mapping - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Test Map ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - HostType: - type: string - HostID: - type: string - ClientType: - type: string - ClientID: - type: string - details: - description: | - Detail payload supports either a flat array/object (treated as new rows) - or an operations object with `created`, `edited`, and `deleted` arrays. - oneOf: - - type: object - properties: - created: - type: array - description: New detail records to insert - items: - type: object - properties: - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - edited: - type: array - description: Existing detail records to update - items: - type: object - properties: - TestMapDetailID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - deleted: - type: array - description: TestMapDetailIDs to soft delete - items: - type: integer - - type: array - description: Shortcut format for creating new details only - items: - type: object - properties: - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - - type: object - description: Shortcut format for creating a single new detail - properties: - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - responses: - '200': - description: Test mapping updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: integer - description: Updated TestMapID - /api/test/testmap/by-testcode/{testCode}: - get: - tags: - - Test - summary: Get test mappings by test code with details - security: - - bearerAuth: [] - parameters: - - name: testCode - in: path - required: true - schema: - type: string - description: Test Code (matches HostTestCode or ClientTestCode) - responses: - '200': - description: List of test mappings with details for the test code - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/TestMap' - /api/test/testmap/detail: - get: - tags: - - Test - summary: List test mapping details - security: - - bearerAuth: [] - parameters: - - name: TestMapID - in: query - schema: - type: integer - description: Filter by TestMapID - responses: - '200': - description: List of test mapping details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/TestMapDetail' - post: - tags: - - Test - summary: Create test mapping detail - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapID: - type: integer - description: Test Map ID (required) - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - required: - - TestMapID - responses: - '201': - description: Test mapping detail created - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: integer - description: Created TestMapDetailID - delete: - tags: - - Test - summary: Soft delete test mapping detail - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapDetailID: - type: integer - description: Test Map Detail ID to delete (required) - required: - - TestMapDetailID - responses: - '200': - description: Test mapping detail deleted - /api/test/testmap/detail/{id}: - get: - tags: - - Test - summary: Get test mapping detail by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Test Map Detail ID - responses: - '200': - description: Test mapping detail - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/TestMapDetail' - patch: - tags: - - Test - summary: Update test mapping detail - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Test Map Detail ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - responses: - '200': - description: Test mapping detail updated - /api/test/testmap/detail/by-testmap/{testMapID}: - get: - tags: - - Test - summary: Get test mapping details by test map ID - security: - - bearerAuth: [] - parameters: - - name: testMapID - in: path - required: true - schema: - type: integer - description: Test Map ID - responses: - '200': - description: List of test mapping details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/TestMapDetail' - /api/test/testmap/detail/batch: - post: - tags: - - Test - summary: Batch create test mapping details - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: array - items: - type: object - properties: - TestMapID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - responses: - '200': - description: Batch create results - patch: - tags: - - Test - summary: Batch update test mapping details - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: array - items: - type: object - properties: - TestMapDetailID: - type: integer - TestMapID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - responses: - '200': - description: Batch update results - delete: - tags: - - Test - summary: Batch delete test mapping details - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: array - items: - type: integer - description: TestMapDetailIDs to delete - responses: - '200': - description: Batch delete results - /api/test: - get: - tags: - - Test - summary: List test definitions - security: - - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number for pagination - - name: perPage - in: query - schema: - type: integer - default: 20 - description: Number of items per page - - name: SiteID - in: query - schema: - type: integer - description: Filter by site ID - - name: TestType - in: query - schema: - type: string - enum: - - TEST - - PARAM - - CALC - - GROUP - - TITLE - description: Filter by test type - - name: isVisibleScr - in: query - schema: - type: integer - enum: - - 0 - - 1 - description: Filter by screen visibility (0=hidden, 1=visible) - - name: isVisibleRpt - in: query - schema: - type: integer - enum: - - 0 - - 1 - description: Filter by report visibility (0=hidden, 1=visible) - - name: search - in: query - schema: - type: string - description: Search by test code or name - responses: - '200': - description: List of test definitions - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - type: array - items: - $ref: '#/components/schemas/TestDefinitionListItem' - pagination: - type: object - properties: - total: - type: integer - description: Total number of records matching the query - examples: - list_flat: - summary: Flat list response from testdefsite - value: - status: success - message: Data fetched successfully - data: - - TestSiteID: 21 - TestSiteCode: GLU - TestSiteName: Glucose - TestType: TEST - SeqScr: 11 - SeqRpt: 11 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - StartDate: '2026-01-01 00:00:00' - EndDate: null - DisciplineID: 2 - DepartmentID: 2 - DisciplineName: Clinical Chemistry - DepartmentName: Laboratory - - TestSiteID: 22 - TestSiteCode: CREA - TestSiteName: Creatinine - TestType: TEST - SeqScr: 12 - SeqRpt: 12 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - StartDate: '2026-01-01 00:00:00' - EndDate: null - DisciplineID: 2 - DepartmentID: 2 - DisciplineName: Clinical Chemistry - DepartmentName: Laboratory - post: - tags: - - Test - summary: Create test definition - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - SiteID: - type: integer - description: Site ID (required) - TestSiteCode: - type: string - description: Test code (required) - TestSiteName: - type: string - description: Test name (required) - TestType: - type: string - enum: - - TEST - - PARAM - - CALC - - GROUP - - TITLE - description: Test type (required) - Description: - type: string - DisciplineID: - type: integer - DepartmentID: - type: integer - ResultType: - type: string - enum: - - NMRIC - - RANGE - - TEXT - - VSET - - NORES - RefType: - type: string - enum: - - RANGE - - THOLD - - VSET - - TEXT - - NOREF - VSet: - type: integer - ReqQty: - type: number - format: decimal - ReqQtyUnit: - type: string - Unit1: - type: string - Factor: - type: number - format: decimal - Unit2: - type: string - Decimal: - type: integer - CollReq: - type: string - Method: - type: string - ExpectedTAT: - type: integer - SeqScr: - type: integer - SeqRpt: - type: integer - IndentLeft: - type: integer - FontStyle: - type: string - isVisibleScr: - type: integer - isVisibleRpt: - type: integer - isCountStat: - type: integer - testdefcal: - type: object - description: Calculated test metadata persisted in the `testdefcal` table. - properties: - FormulaCode: - type: string - description: Formula expression for calculated tests (e.g., "{TBIL} - {DBIL}") - testdefgrp: - type: object - description: Group definition wrapper for CALC/GROUP member assignments. - properties: - members: - type: array - description: Array of member TestSiteIDs for CALC/GROUP definitions. - items: - type: object - properties: - TestSiteID: - type: integer - description: Foreign key referencing the member test's TestSiteID. - required: - - TestSiteID - refnum: - type: array - items: - type: object - reftxt: - type: array - items: - type: object - testmap: - type: array - items: - type: object - properties: - HostType: - type: string - HostID: - type: string - HostTestCode: - type: string - HostTestName: - type: string - ClientType: - type: string - ClientID: - type: string - ClientTestCode: - type: string - ClientTestName: - type: string - ConDefID: - type: integer - nullable: true - required: - - SiteID - - TestSiteCode - - TestSiteName - - TestType - examples: - TEST_no_ref: - summary: Technical test without reference or map - value: - SiteID: 1 - TestSiteCode: TEST_NREF - TestSiteName: Numeric Test - TestType: TEST - SeqScr: 500 - SeqRpt: 500 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - DisciplineID: 2 - DepartmentID: 2 - Unit1: mg/dL - Method: CBC Analyzer - PARAM_no_ref: - summary: Parameter without reference or map - value: - SiteID: 1 - TestSiteCode: PARAM_NRF - TestSiteName: Clinical Parameter - TestType: PARAM - SeqScr: 10 - SeqRpt: 10 - isVisibleScr: 1 - isVisibleRpt: 0 - isCountStat: 0 - DisciplineID: 10 - DepartmentID: 0 - Unit1: cm - Method: Manual entry - TEST_range_single: - summary: Technical test with numeric range reference (single) - value: - SiteID: 1 - TestSiteCode: TEST_RANGE - TestSiteName: Glucose Range - TestType: TEST - SeqScr: 105 - SeqRpt: 105 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - refnum: - - NumRefType: NMRC - RangeType: REF - Sex: '2' - LowSign: GE - Low: 70 - HighSign: LE - High: 100 - AgeStart: 6570 - AgeEnd: 36135 - Flag: 'N' - DisciplineID: 2 - DepartmentID: 2 - ResultType: NMRIC - RefType: RANGE - Unit1: mg/dL - Method: Hexokinase - TEST_range_multiple_map: - summary: Numeric reference with multiple ranges and test map - value: - SiteID: 1 - TestSiteCode: TEST_RMAP - TestSiteName: Glucose Panic Range - TestType: TEST - SeqScr: 110 - SeqRpt: 110 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - refnum: - - NumRefType: NMRC - RangeType: REF - Sex: '2' - LowSign: GE - Low: 70 - HighSign: LE - High: 100 - AgeStart: 6570 - AgeEnd: 36135 - Flag: 'N' - - NumRefType: NMRC - RangeType: REF - Sex: '1' - LowSign: '>' - Low: 75 - HighSign: < - High: 105 - AgeStart: 6570 - AgeEnd: 36135 - Flag: 'N' - testmap: - - HostType: SITE - HostID: '1' - HostTestCode: GLU - HostTestName: Glucose - ClientType: WST - ClientID: '1' - ClientTestCode: GLU_C - ClientTestName: Glucose Client - ConDefID: 1 - - HostType: SITE - HostID: '1' - HostTestCode: CREA - HostTestName: Creatinine - ClientType: WST - ClientID: '1' - ClientTestCode: CREA_C - ClientTestName: Creatinine Client - ConDefID: 2 - - HostType: WST - HostID: '3' - HostTestCode: HB - HostTestName: Hemoglobin - ClientType: INST - ClientID: '2' - ClientTestCode: HB_C - ClientTestName: Hemoglobin Client - ConDefID: 3 - DisciplineID: 2 - DepartmentID: 2 - ResultType: NMRIC - RefType: RANGE - Unit1: mg/dL - Method: Hexokinase - TEST_threshold: - summary: Technical test with threshold reference - value: - SiteID: 1 - TestSiteCode: TEST_THLD - TestSiteName: Sodium Threshold - TestType: TEST - SeqScr: 115 - SeqRpt: 115 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - refnum: - - NumRefType: THOLD - RangeType: PANIC - Sex: '2' - LowSign: LT - Low: 120 - AgeStart: 0 - AgeEnd: 45625 - Flag: H - DisciplineID: 2 - DepartmentID: 2 - ResultType: NMRIC - RefType: THOLD - Unit1: mmol/L - Method: Auto Analyzer - TEST_threshold_map: - summary: Threshold reference plus test map - value: - SiteID: 1 - TestSiteCode: TEST_TMAP - TestSiteName: Potassium Panic - TestType: TEST - SeqScr: 120 - SeqRpt: 120 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - refnum: - - NumRefType: THOLD - RangeType: PANIC - Sex: '2' - LowSign: LT - Low: 120 - AgeStart: 0 - AgeEnd: 45625 - Flag: H - - NumRefType: THOLD - RangeType: PANIC - Sex: '1' - LowSign: < - Low: 121 - AgeStart: 0 - AgeEnd: 45625 - Flag: H - testmap: - - HostType: SITE - HostID: '1' - ClientType: WST - ClientID: '1' - details: - - HostTestCode: HB - HostTestName: Hemoglobin - ConDefID: 3 - ClientTestCode: HB_C - ClientTestName: Hemoglobin Client - - HostTestCode: GLU - HostTestName: Glucose - ConDefID: 1 - ClientTestCode: GLU_C - ClientTestName: Glucose Client - DisciplineID: 2 - DepartmentID: 2 - ResultType: NMRIC - RefType: THOLD - Unit1: mmol/L - Method: Auto Analyzer - TEST_text: - summary: Technical test with text reference - value: - SiteID: 1 - TestSiteCode: TEST_TEXT - TestSiteName: Disease Stage - TestType: TEST - SeqScr: 130 - SeqRpt: 130 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - reftxt: - - SpcType: GEN - TxtRefType: TEXT - Sex: '2' - AgeStart: 6570 - AgeEnd: 36135 - RefTxt: NORM=Normal;HIGH=High - Flag: 'N' - DisciplineID: 1 - DepartmentID: 1 - ResultType: TEXT - RefType: TEXT - Method: Morphology - TEST_text_map: - summary: Text reference plus test map - value: - SiteID: 1 - TestSiteCode: TEST_TXM - TestSiteName: Disease Stage (Map) - TestType: TEST - SeqScr: 135 - SeqRpt: 135 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - reftxt: - - SpcType: GEN - TxtRefType: TEXT - Sex: '2' - AgeStart: 6570 - AgeEnd: 36135 - RefTxt: NORM=Normal - Flag: 'N' - - SpcType: GEN - TxtRefType: TEXT - Sex: '1' - AgeStart: 6570 - AgeEnd: 36135 - RefTxt: ABN=Abnormal - Flag: 'N' - testmap: - - HostType: SITE - HostID: '1' - ClientType: WST - ClientID: '1' - details: - - HostTestCode: STAGE - HostTestName: Disease Stage - ConDefID: 4 - ClientTestCode: STAGE_C - ClientTestName: Disease Stage Client - DisciplineID: 1 - DepartmentID: 1 - ResultType: TEXT - RefType: TEXT - TEST_valueset: - summary: Technical test using a value set result - value: - SiteID: 1 - TestSiteCode: TEST_VSET - TestSiteName: Urine Color - TestType: TEST - SeqScr: 140 - SeqRpt: 140 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - reftxt: - - SpcType: GEN - TxtRefType: VSET - Sex: '2' - AgeStart: 0 - AgeEnd: 43800 - RefTxt: NORM=Normal;MACRO=Macro - Flag: 'N' - DisciplineID: 4 - DepartmentID: 4 - ResultType: VSET - RefType: VSET - Method: Visual - TEST_valueset_map: - summary: Value set reference with test map - value: - SiteID: 1 - TestSiteCode: TEST_VMAP - TestSiteName: Urine Color (Map) - TestType: TEST - SeqScr: 145 - SeqRpt: 145 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - reftxt: - - SpcType: GEN - TxtRefType: VSET - Sex: '2' - AgeStart: 0 - AgeEnd: 43800 - RefTxt: NORM=Normal;ABN=Abnormal - Flag: 'N' - testmap: - - HostType: SITE - HostID: '1' - ClientType: WST - ClientID: '8' - details: - - HostTestCode: UCOLOR - HostTestName: Urine Color - ConDefID: 12 - ClientTestCode: UCOLOR_C - ClientTestName: Urine Color Client - DisciplineID: 4 - DepartmentID: 4 - ResultType: VSET - RefType: VSET - TEST_valueset_map_no_reftxt: - summary: Value set result with mapping but without explicit text reference entries - value: - SiteID: 1 - TestSiteCode: TEST_VSETM - TestSiteName: Urine Result Map - TestType: TEST - SeqScr: 150 - SeqRpt: 150 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - testmap: - - HostType: SITE - HostID: '1' - ClientType: WST - ClientID: '8' - details: - - HostTestCode: UGLUC - HostTestName: Urine Glucose - ConDefID: 12 - ClientTestCode: UGLUC_C - ClientTestName: Urine Glucose Client - DisciplineID: 4 - DepartmentID: 4 - ResultType: VSET - RefType: VSET - CALC_basic: - summary: Calculated test with members (no references) - value: - SiteID: 1 - TestSiteCode: CALC_BASE - TestSiteName: Estimated GFR - TestType: CALC - SeqScr: 190 - SeqRpt: 190 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 0 - DisciplineID: 2 - DepartmentID: 2 - testdefcal: - FormulaCode: CKD_EPI(CREA,AGE,GENDER) - testdefgrp: - members: - - TestSiteID: 21 - - TestSiteID: 22 - CALC_full: - summary: Calculated test with numeric reference ranges and map - value: - SiteID: 1 - TestSiteCode: CALC_FULL - TestSiteName: Estimated GFR (Map) - TestType: CALC - SeqScr: 195 - SeqRpt: 195 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 0 - refnum: - - NumRefType: NMRC - RangeType: REF - Sex: '2' - LowSign: GE - Low: 10 - HighSign: LE - High: 20 - AgeStart: 6570 - AgeEnd: 43800 - Flag: 'N' - testmap: - - HostType: SITE - HostID: '1' - ClientType: WST - ClientID: '3' - details: - - HostTestCode: EGFR - HostTestName: eGFR - ConDefID: 1 - ClientTestCode: EGFR_C - ClientTestName: eGFR Client - DisciplineID: 2 - DepartmentID: 2 - testdefcal: - FormulaCode: CKD_EPI(CREA,AGE,GENDER) - testdefgrp: - members: - - TestSiteID: 21 - - TestSiteID: 22 - GROUP_with_members: - summary: Group/profile test with members and mapping - value: - SiteID: 1 - TestSiteCode: GROUP_PNL - TestSiteName: Lipid Profile - TestType: GROUP - SeqScr: 10 - SeqRpt: 10 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - testmap: - - HostType: SITE - HostID: '1' - ClientType: WST - ClientID: '3' - details: - - HostTestCode: LIPID - HostTestName: Lipid Profile - ConDefID: 1 - ClientTestCode: LIPID_C - ClientTestName: Lipid Client - testdefgrp: - members: - - TestSiteID: 169 - - TestSiteID: 170 - responses: - '201': - description: Test definition created - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: created - message: - type: string - data: - type: object - properties: - TestSiteId: - type: integer - '400': - description: Validation error (e.g., invalid member TestSiteID) - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: failed - message: - type: string - example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' - /api/test/{id}: - get: - tags: - - Test - summary: Get test definition by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Test Site ID - responses: - '200': - description: Test definition details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/TestDefinition' - '404': - description: Test not found - patch: - tags: - - Test - summary: Update test definition - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Test Site ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestSiteID: - type: integer - description: Test Site ID (required) - TestSiteCode: - type: string - TestSiteName: - type: string - TestType: - type: string - enum: - - TEST - - PARAM - - CALC - - GROUP - - TITLE - Description: - type: string - DisciplineID: - type: integer - DepartmentID: - type: integer - ResultType: - type: string - enum: - - NMRIC - - RANGE - - TEXT - - VSET - - NORES - RefType: - type: string - enum: - - RANGE - - THOLD - - VSET - - TEXT - - NOREF - VSet: - type: integer - ReqQty: - type: number - format: decimal - ReqQtyUnit: - type: string - Unit1: - type: string - Factor: - type: number - format: decimal - Unit2: - type: string - Decimal: - type: integer - CollReq: - type: string - Method: - type: string - ExpectedTAT: - type: integer - SeqScr: - type: integer - SeqRpt: - type: integer - IndentLeft: - type: integer - FontStyle: - type: string - isVisibleScr: - type: integer - isVisibleRpt: - type: integer - isCountStat: - type: integer - testdefcal: - type: object - description: Calculated test metadata persisted in the `testdefcal` table. - properties: - FormulaCode: - type: string - description: Formula expression for calculated tests (e.g., "{TBIL} - {DBIL}") - testdefgrp: - type: object - description: Group definition wrapper for CALC/GROUP member assignments. - properties: - members: - type: array - description: Array of member TestSiteIDs for CALC/GROUP definitions. - items: - type: object - properties: - TestSiteID: - type: integer - description: Foreign key referencing the member test's TestSiteID. - required: - - TestSiteID - refnum: - type: array - items: - type: object - reftxt: - type: array - items: - type: object - testmap: - type: array - items: - type: object - properties: - HostType: - type: string - HostID: - type: string - HostTestCode: - type: string - HostTestName: - type: string - ClientType: - type: string - ClientID: - type: string - ClientTestCode: - type: string - ClientTestName: - type: string - ConDefID: - type: integer - nullable: true - required: - - TestSiteID - responses: - '200': - description: Test definition updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: object - properties: - TestSiteId: - type: integer - '400': - description: Validation error (e.g., invalid member TestSiteID) - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: failed - message: - type: string - example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' - delete: - tags: - - Test - summary: Soft delete test definition - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Test Site ID to delete - requestBody: - content: - application/json: - schema: - type: object - properties: - TestSiteID: - type: integer - description: Optional - can be provided in body instead of path - responses: - '200': - description: Test disabled successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: object - properties: - TestSiteId: - type: integer - EndDate: - type: string - format: date-time - '404': - description: Test not found - '422': - description: Test already disabled - /api/user: - get: - tags: - - User - summary: List users with pagination and search - security: - - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number - - name: per_page - in: query - schema: - type: integer - default: 20 - description: Items per page - - name: search - in: query - schema: - type: string - description: Search term for username, email, or name - responses: - '200': - description: List of users with pagination - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Users retrieved successfully - data: - type: object - properties: - users: - type: array - items: - $ref: '#/components/schemas/User' - pagination: - type: object - properties: - current_page: - type: integer - per_page: - type: integer - total: - type: integer - total_pages: - type: integer - '500': - description: Server error - post: - tags: - - User - summary: Create new user - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreate' - responses: - '201': - description: User created successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: User created successfully - data: - type: object - properties: - UserID: - type: integer - Username: - type: string - Email: - type: string - '400': - description: Validation failed - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: failed - message: - type: string - example: Validation failed - data: - type: object - '500': - description: Server error - /api/user/{id}: - get: - tags: - - User - summary: Get user by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: User ID - responses: - '200': - description: User details - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '404': - description: User not found - '500': - description: Server error - patch: - tags: - - User - summary: Update existing user - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: User ID - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdate' - responses: - '200': - description: User updated successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: User updated successfully - data: - type: object - properties: - UserID: - type: integer - updated_fields: - type: array - items: - type: string - '400': - description: UserID is required - '404': - description: User not found - '500': - description: Server error - delete: - tags: - - User - summary: Delete user (soft delete) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: User ID - responses: - '200': - description: User deleted successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: User deleted successfully - data: - type: object - properties: - UserID: - type: integer - '404': - description: User not found - '500': - description: Server error - /api/valueset: - get: - tags: - - ValueSet - summary: List lib value sets - description: List all library/system value sets from JSON files with item counts. Returns an array of objects with value, label, and count properties. - security: - - bearerAuth: [] - parameters: - - name: search - in: query - schema: - type: string - description: Optional search term to filter value set names or labels - responses: - '200': - description: List of lib value sets with item counts - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - data: - type: array - items: - $ref: '#/components/schemas/ValueSetListItem' - example: - - value: sex - label: Sex - count: 3 - - value: marital_status - label: Marital Status - count: 6 - - value: order_status - label: Order Status - count: 6 - /api/valueset/{key}: - get: - tags: - - ValueSet - summary: Get lib value set by key - description: | - Get a specific library/system value set from JSON files. - - **Available value set keys:** - - `activity_result` - Activity Result - - `additive` - Additive - - `adt_event` - ADT Event - - `area_class` - Area Class - - `body_site` - Body Site - - `collection_method` - Collection Method - - `container_cap_color` - Container Cap Color - - `container_class` - Container Class - - `container_size` - Container Size - - `country` - Country - - `death_indicator` - Death Indicator - - `did_type` - DID Type - - `enable_disable` - Enable/Disable - - `entity_type` - Entity Type - - `ethnic` - Ethnic - - `fasting_status` - Fasting Status - - `formula_language` - Formula Language - - `generate_by` - Generate By - - `identifier_type` - Identifier Type - - `location_type` - Location Type - - `marital_status` - Marital Status - - `math_sign` - Math Sign - - `numeric_ref_type` - Numeric Reference Type - - `operation` - Operation (CRUD) - - `order_priority` - Order Priority - - `order_status` - Order Status - - `race` - Race (Ethnicity) - - `range_type` - Range Type - - `reference_type` - Reference Type - - `religion` - Religion - - `requested_entity` - Requested Entity - - `result_type` - Result Type - - `result_unit` - Result Unit - - `sex` - Sex - - `site_class` - Site Class - - `site_type` - Site Type - - `specimen_activity` - Specimen Activity - - `specimen_condition` - Specimen Condition - - `specimen_role` - Specimen Role - - `specimen_status` - Specimen Status - - `specimen_type` - Specimen Type - - `test_activity` - Test Activity - - `test_type` - Test Type - - `text_ref_type` - Text Reference Type - - `unit` - Unit - - `v_category` - VCategory - - `ws_type` - Workstation Type - security: - - bearerAuth: [] - parameters: - - name: key - in: path - required: true - schema: - type: string - enum: - - activity_result - - additive - - adt_event - - area_class - - body_site - - collection_method - - container_cap_color - - container_class - - container_size - - country - - death_indicator - - did_type - - enable_disable - - entity_type - - ethnic - - fasting_status - - formula_language - - generate_by - - identifier_type - - location_type - - marital_status - - math_sign - - numeric_ref_type - - operation - - order_priority - - order_status - - race - - range_type - - reference_type - - religion - - requested_entity - - result_type - - result_unit - - sex - - site_class - - site_type - - specimen_activity - - specimen_condition - - specimen_role - - specimen_status - - specimen_type - - test_activity - - test_type - - text_ref_type - - unit - - v_category - - ws_type - description: Value set key name - responses: - '200': - description: Lib value set details - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - type: array - items: - $ref: '#/components/schemas/ValueSetLibItem' - /api/valueset/refresh: - post: - tags: - - ValueSet - summary: Refresh lib ValueSet cache - description: Clear and reload the library/system ValueSet cache from JSON files. Call this after modifying JSON files in app/Libraries/Data/. - security: - - bearerAuth: [] - responses: - '200': - description: Lib ValueSet cache refreshed - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Cache cleared - /api/valueset/user/items: - get: - tags: - - ValueSet - summary: List user value set items - description: List value set items from database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: VSetID - in: query - schema: - type: integer - description: Filter by ValueSet ID - - name: search - in: query - schema: - type: string - description: Search term to filter by VValue, VDesc, or VSName - - name: param - in: query - schema: - type: string - description: Alternative search parameter (alias for search) - responses: - '200': - description: List of user value set items - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - type: array - items: - $ref: '#/components/schemas/ValueSetItem' - post: - tags: - - ValueSet - summary: Create user value set item - description: Create value set item in database (user-defined) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - VSetID - properties: - SiteID: - type: integer - description: Site reference (default 1) - VSetID: - type: integer - description: Reference to value set definition (required) - VOrder: - type: integer - description: Display order (default 0) - VValue: - type: string - description: The value code - VDesc: - type: string - description: The display description/label - responses: - '201': - description: User value set item created - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/ValueSetItem' - /api/valueset/user/items/{id}: - get: - tags: - - ValueSet - summary: Get user value set item by ID - description: Get value set item from database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: User value set item details - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - $ref: '#/components/schemas/ValueSetItem' - put: - tags: - - ValueSet - summary: Update user value set item - description: Update value set item in database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - SiteID: - type: integer - description: Site reference - VSetID: - type: integer - description: Reference to value set definition - VOrder: - type: integer - description: Display order - VValue: - type: string - description: The value code - VDesc: - type: string - description: The display description/label - responses: - '200': - description: User value set item updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/ValueSetItem' - delete: - tags: - - ValueSet - summary: Delete user value set item - description: Delete value set item from database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: User value set item deleted - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - /api/valueset/user/def: - get: - tags: - - ValueSet - summary: List user value set definitions - description: List value set definitions from database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: search - in: query - schema: - type: string - description: Optional search term to filter definitions - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number for pagination - - name: limit - in: query - schema: - type: integer - default: 100 - description: Number of items per page - responses: - '200': - description: List of user value set definitions - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - type: array - items: - $ref: '#/components/schemas/ValueSetDef' - meta: - type: object - properties: - total: - type: integer - page: - type: integer - limit: - type: integer - post: - tags: - - ValueSet - summary: Create user value set definition - description: Create value set definition in database (user-defined) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - SiteID: - type: integer - description: Site reference (default 1) - VSName: - type: string - description: Value set name - VSDesc: - type: string - description: Value set description - responses: - '201': - description: User value set definition created - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/ValueSetDef' - /api/valueset/user/def/{id}: - get: - tags: - - ValueSet - summary: Get user value set definition by ID - description: Get value set definition from database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: User value set definition details - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - $ref: '#/components/schemas/ValueSetDef' - put: - tags: - - ValueSet - summary: Update user value set definition - description: Update value set definition in database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - SiteID: - type: integer - description: Site reference - VSName: - type: string - description: Value set name - VSDesc: - type: string - description: Value set description - responses: - '200': - description: User value set definition updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/ValueSetDef' - delete: - tags: - - ValueSet - summary: Delete user value set definition - description: Delete value set definition from database (user-defined) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: User value set definition deleted - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: JWT token from login endpoint - cookieAuth: - type: apiKey - in: cookie - name: token - description: JWT token stored in HTTP-only cookie - schemas: - SuccessResponse: - type: object - properties: - status: - type: string - example: success - message: - type: string - code: - type: integer - example: 200 - ErrorResponse: - type: object - properties: - status: - type: string - example: error - message: - type: string - errors: - type: object - DashboardSummary: - type: object - properties: - pendingOrders: - type: integer - todayResults: - type: integer - criticalResults: - type: integer - activePatients: - type: integer - LoginRequest: - type: object - required: - - username - - password - properties: - username: - type: string - example: labuser01 - password: - type: string - format: password - example: secret123 - LoginResponse: - type: object - properties: - status: - type: string - example: success - code: - type: integer - example: 200 - message: - type: string - example: Login successful - RegisterRequest: - type: object - required: - - username - - password - - email - properties: - username: - type: string - password: - type: string - format: password - email: - type: string - format: email - full_name: - type: string - Patient: - type: object - required: - - PatientID - - Sex - - NameFirst - - Birthdate - properties: - PatientID: - type: string - maxLength: 30 - pattern: ^[A-Za-z0-9]+$ - description: Internal patient identifier - AlternatePID: - type: string - maxLength: 30 - pattern: ^[A-Za-z0-9]+$ - Prefix: - type: string - maxLength: 10 - enum: - - Mr - - Mrs - - Ms - - Dr - - Prof - Sex: - type: string - enum: - - '1' - - '2' - description: '1: Female, 2: Male' - NameFirst: - type: string - minLength: 1 - maxLength: 60 - pattern: ^[A-Za-z'\. ]+$ - NameMiddle: - type: string - minLength: 1 - maxLength: 60 - NameMaiden: - type: string - minLength: 1 - maxLength: 60 - NameLast: - type: string - minLength: 1 - maxLength: 60 - Suffix: - type: string - maxLength: 10 - Birthdate: - type: string - format: date-time - description: ISO 8601 UTC datetime - PlaceOfBirth: - type: string - maxLength: 100 - Citizenship: - type: string - maxLength: 100 - Street_1: - type: string - maxLength: 255 - Street_2: - type: string - maxLength: 255 - Street_3: - type: string - maxLength: 255 - ZIP: - type: string - maxLength: 10 - pattern: ^[0-9]+$ - Phone: - type: string - pattern: ^\+?[0-9]{8,15}$ - MobilePhone: - type: string - pattern: ^\+?[0-9]{8,15}$ - EmailAddress1: - type: string - format: email - maxLength: 100 - EmailAddress2: - type: string - format: email - maxLength: 100 - PatIdt: - $ref: '#/components/schemas/PatientIdentifier' - LinkTo: - type: array - description: Array of linked patient references - items: - $ref: '#/components/schemas/LinkedPatient' - Custodian: - $ref: '#/components/schemas/Custodian' - isDead: - type: string - enum: - - '0' - - '1' - description: '0: No (alive), 1: Yes (deceased)' - TimeOfDeath: - type: string - format: date-time - description: ISO 8601 UTC datetime of death - PatCom: - type: string - description: Patient comment/notes - PatAtt: - type: array - description: Patient address entries - items: - $ref: '#/components/schemas/PatAttEntry' - Province: - type: integer - description: Province AreaGeoID (foreign key to areageo table) - ProvinceLabel: - type: string - description: Province name (resolved from areageo) - City: - type: integer - description: City AreaGeoID (foreign key to areageo table) - CityLabel: - type: string - description: City name (resolved from areageo) - Country: - type: string - maxLength: 10 - description: Country ISO 3-letter code (e.g., IDN, USA) - CountryLabel: - type: string - description: Country name (resolved from valueset) - Race: - type: string - maxLength: 100 - MaritalStatus: - type: string - enum: - - A - - B - - D - - M - - S - - W - description: 'A: Annulled, B: Separated, D: Divorced, M: Married, S: Single, W: Widowed' - Religion: - type: string - maxLength: 100 - Ethnic: - type: string - maxLength: 100 - PatientIdentifier: - type: object - properties: - IdentifierType: - type: string - enum: - - KTP - - PASS - - SSN - - SIM - - KTAS - description: | - KTP: 16 digit numeric - PASS: alphanumeric max 9 - SSN: 9 digit numeric - SIM: 19-20 digit numeric - KTAS: 11 digit numeric - Identifier: - type: string - maxLength: 255 - LinkedPatient: - type: object - description: Linked patient reference - properties: - InternalPID: - type: integer - description: Internal patient ID of the linked patient - PatientID: - type: string - description: Patient ID of the linked patient - Custodian: - type: object - description: Patient custodian/guardian - properties: - InternalPID: - type: integer - description: Internal patient ID of the custodian - PatientID: - type: string - description: Patient ID of the custodian - PatAttEntry: - type: object - description: Patient address/attorney entry - properties: - Address: - type: string - description: Address text - PatientListResponse: - type: object - properties: - status: - type: string - example: success - data: - type: array - items: - $ref: '#/components/schemas/Patient' - pagination: - type: object - properties: - page: - type: integer - perPage: - type: integer - total: - type: integer - PatientVisit: - type: object - properties: - InternalPVID: - type: integer - description: Primary key (auto-generated) - PVID: - type: string - description: Visit ID (auto-generated with DV prefix if not provided) - InternalPID: - type: integer - description: Reference to patient - EpisodeID: - type: string - description: Episode identifier - SiteID: - type: integer - description: Site reference - LastLocation: - type: string - description: Full name of the last/current location from patvisitadt - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - ArchivedDate: - type: string - format: date-time - DelDate: - type: string - format: date-time - PatDiag: - type: object - description: Diagnosis information (optional) - properties: - DiagCode: - type: string - Diagnosis: - type: string - PatVisitADT: - type: object - description: ADT (Admission/Discharge/Transfer) information (optional) - properties: - ADTCode: - type: string - enum: - - A01 - - A02 - - A03 - - A04 - - A08 - LocationID: - type: integer - AttDoc: - type: integer - description: Attending physician ContactID - RefDoc: - type: integer - description: Referring physician ContactID - AdmDoc: - type: integer - description: Admitting physician ContactID - CnsDoc: - type: integer - description: Consulting physician ContactID - PatVisitADT: - type: object - properties: - PVADTID: - type: integer - description: Primary key (auto-generated) - InternalPVID: - type: integer - description: Reference to patient visit - ADTCode: - type: string - enum: - - A01 - - A02 - - A03 - - A04 - - A08 - description: | - A01: Admit - A02: Transfer - A03: Discharge - A04: Register - A08: Update - LocationID: - type: integer - description: Location/ward reference - AttDoc: - type: integer - description: Attending physician ContactID - RefDoc: - type: integer - description: Referring physician ContactID - AdmDoc: - type: integer - description: Admitting physician ContactID - CnsDoc: - type: integer - description: Consulting physician ContactID - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - ArchivedDate: - type: string - format: date-time - DelDate: - type: string - format: date-time - Account: - type: object - properties: - id: - type: integer - AccountName: - type: string - AccountCode: - type: string - AccountType: - type: string - Site: - type: object - properties: - id: - type: integer - SiteName: - type: string - SiteCode: - type: string - maxLength: 2 - pattern: ^[A-Z0-9]{2}$ - AccountID: - type: integer - Discipline: - type: object - properties: - id: - type: integer - DisciplineName: - type: string - DisciplineCode: - type: string - SeqScr: - type: integer - description: Display order on screen - SeqRpt: - type: integer - description: Display order in reports - Department: - type: object - properties: - id: - type: integer - DeptName: - type: string - DeptCode: - type: string - SiteID: - type: integer - Workstation: - type: object - properties: - id: - type: integer - WorkstationName: - type: string - WorkstationCode: - type: string - SiteID: - type: integer - DepartmentID: - type: integer - HostApp: - type: object - properties: - HostAppID: - type: string - maxLength: 5 - HostAppName: - type: string - SiteID: - type: integer - SiteName: - type: string - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - HostComPara: - type: object - properties: - HostAppID: - type: string - maxLength: 5 - HostAppName: - type: string - HostIP: - type: string - maxLength: 15 - HostPort: - type: string - maxLength: 6 - HostPwd: - type: string - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - CodingSys: - type: object - properties: - CodingSysID: - type: integer - CodingSysAbb: - type: string - maxLength: 6 - FullText: - type: string - Description: - type: string - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - Specimen: - type: object - properties: - id: - type: integer - SpecimenID: - type: string - PatientID: - type: string - SpecimenType: - type: string - description: Specimen type code - SpecimenTypeLabel: - type: string - description: Specimen type display text - CollectionDate: - type: string - format: date-time - CollectionMethod: - type: string - description: Collection method code - CollectionMethodLabel: - type: string - description: Collection method display text - ContainerID: - type: integer - SpecimenStatus: - type: string - description: Specimen status code - SpecimenStatusLabel: - type: string - description: Specimen status display text - BodySite: - type: string - description: Body site code - BodySiteLabel: - type: string - description: Body site display text - ContainerDef: - type: object - properties: - id: - type: integer - ContainerCode: - type: string - ContainerName: - type: string - ConCategory: - type: string - description: Container category code - ConCategoryLabel: - type: string - description: Container category display text - ConSize: - type: string - description: Container size code - ConSizeLabel: - type: string - description: Container size display text - CapColor: - type: string - description: Cap color code - CapColorLabel: - type: string - description: Cap color display text - SpecimenPrep: - type: object - properties: - id: - type: integer - PrepCode: - type: string - PrepName: - type: string - Description: - type: string - SpecimenStatus: - type: object - properties: - id: - type: integer - StatusCode: - type: string - StatusName: - type: string - Description: - type: string - Status: - type: string - description: Status code - StatusLabel: - type: string - description: Status display text - Activity: - type: string - description: Activity code - ActivityLabel: - type: string - description: Activity display text - SpecimenCollection: - type: object - properties: - id: - type: integer - CollectionCode: - type: string - CollectionName: - type: string - Description: - type: string - CollectionMethod: - type: string - description: Collection method code - CollectionMethodLabel: - type: string - description: Collection method display text - Additive: - type: string - description: Additive code - AdditiveLabel: - type: string - description: Additive display text - SpecimenRole: - type: string - description: Specimen role code - SpecimenRoleLabel: - type: string - description: Specimen role display text - TestDefinition: - type: object - properties: - TestSiteID: - type: integer - SiteID: - type: integer - TestSiteCode: - type: string - TestSiteName: - type: string - TestType: - type: string - enum: - - TEST - - PARAM - - CALC - - GROUP - - TITLE - description: | - TEST: Technical test - PARAM: Parameter - CALC: Calculated - GROUP: Panel/Profile - TITLE: Section header - Description: - type: string - DisciplineID: - type: integer - DisciplineName: - type: string - DepartmentID: - type: integer - DepartmentName: - type: string - ResultType: - type: string - enum: - - NMRIC - - RANGE - - TEXT - - VSET - - NORES - description: | - Result type determines the format of test results: - - NMRIC: Single numeric value - - RANGE: Numeric range (min-max) - - TEXT: Free text result - - VSET: Value set/enum result - - NORES: No result (for GROUP and TITLE types) - - TestType to ResultType mapping: - - TEST: NMRIC | RANGE | TEXT | VSET - - PARAM: NMRIC | RANGE | TEXT | VSET - - CALC: NMRIC (calculated result is always numeric) - - GROUP: NORES (no result, container only) - - TITLE: NORES (no result, header only) - RefType: - type: string - enum: - - RANGE - - THOLD - - VSET - - TEXT - - NOREF - description: | - Reference type determines which reference range table to use: - - RANGE: Numeric reference range - - THOLD: Threshold/panic range - - VSET: Value set reference - - TEXT: Free text reference - - NOREF: No reference (for NORES result type) - - ResultType to RefType mapping: - - NMRIC: RANGE | THOLD → refnum table - - RANGE: RANGE | THOLD → refnum table - - VSET: VSET → reftxt table - - TEXT: TEXT → reftxt table - - NORES: NOREF → (no reference table) - VSet: - type: integer - description: Value set ID for VSET result type - ReqQty: - type: number - format: decimal - description: Required sample quantity - ReqQtyUnit: - type: string - description: Unit for required quantity - Unit1: - type: string - description: Primary unit - Factor: - type: number - format: decimal - description: Conversion factor - Unit2: - type: string - description: Secondary unit (after conversion) - Decimal: - type: integer - description: Number of decimal places - CollReq: - type: string - description: Collection requirements - Method: - type: string - description: Test method - ExpectedTAT: - type: integer - description: Expected turnaround time - SeqScr: - type: integer - description: Screen sequence - SeqRpt: - type: integer - description: Report sequence - IndentLeft: - type: integer - default: 0 - FontStyle: - type: string - isVisibleScr: - type: integer - default: 1 - description: Screen visibility (0=hidden, 1=visible) - isVisibleRpt: - type: integer - default: 1 - description: Report visibility (0=hidden, 1=visible) - isCountStat: - type: integer - default: 1 - Level: - type: integer - isRequestable: - type: integer - default: 1 - description: Flag indicating if test can be requested (1=yes, 0=no) - CreateDate: - type: string - format: date-time - StartDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - FormulaCode: - type: string - description: Formula expression for calculated tests - testdefcal: - type: array - description: Calculated test details (only for CALC type) - items: - type: object - testdefgrp: - type: object - description: Group definition payload for GROUP and CALC types. - properties: - members: - type: array - description: | - Group members (for GROUP and CALC types). - When creating or updating, provide members in testdefgrp.members with TestSiteID field. - Do NOT use Member, SeqScr, or Members fields when creating/updating. - items: - type: object - properties: - TestGrpID: - type: integer - description: Group membership record ID - TestSiteID: - type: integer - description: Parent group TestSiteID - Member: - type: integer - description: | - Member TestSiteID (foreign key to testdefsite). - **Note**: This field is in the response. When creating/updating, use TestSiteID in testdefgrp.members instead. - TestSiteCode: - type: string - description: Member test code - TestSiteName: - type: string - description: Member test name - TestType: - type: string - description: Member test type - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - testmap: - type: array - description: Flat test mapping payload for /api/test create/update - items: - type: object - properties: - HostType: - type: string - HostID: - type: string - HostTestCode: - type: string - HostTestName: - type: string - ClientType: - type: string - ClientID: - type: string - ClientTestCode: - type: string - ClientTestName: - type: string - ConDefID: - type: integer - nullable: true - refnum: - type: array - description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type. - items: - type: object - properties: - RefNumID: - type: integer - NumRefType: - type: string - enum: - - NMRC - - THOLD - description: NMRC=Numeric range, THOLD=Threshold - NumRefTypeLabel: - type: string - RangeType: - type: string - RangeTypeLabel: - type: string - SpcType: - type: string - description: Specimen type code (e.g., GEN, EDTA) - Sex: - type: string - SexLabel: - type: string - LowSign: - type: string - LowSignLabel: - type: string - HighSign: - type: string - HighSignLabel: - type: string - High: - type: number - format: float - Low: - type: number - format: float - AgeStart: - type: integer - description: Minimum patient age in days - AgeEnd: - type: integer - description: Maximum patient age in days - Flag: - type: string - Interpretation: - type: string - Notes: - type: string - description: Optional note attached to the numeric reference range - reftxt: - type: array - description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type. - items: - type: object - properties: - RefTxtID: - type: integer - TxtRefType: - type: string - enum: - - TEXT - - VSET - description: TEXT=Free text, VSET=Value set - TxtRefTypeLabel: - type: string - Sex: - type: string - SexLabel: - type: string - AgeStart: - type: integer - description: Minimum patient age in days - AgeEnd: - type: integer - description: Maximum patient age in days - RefTxt: - type: string - Flag: - type: string - examples: - TEST_numeric: - summary: Technical test with numeric reference - value: - TestSiteID: 1 - SiteID: 1 - TestSiteCode: GLU - TestSiteName: Glucose - TestType: TEST - DisciplineID: 2 - DepartmentID: 2 - ResultType: NMRIC - RefType: NMRC - Unit1: mg/dL - ReqQty: 300 - ReqQtyUnit: uL - Decimal: 0 - Method: Hexokinase - SeqScr: 11 - SeqRpt: 11 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - refnum: - - RefNumID: 1 - NumRefType: NMRC - NumRefTypeLabel: Numeric - RangeType: REF - RangeTypeLabel: Reference Range - Sex: '2' - SexLabel: Male - LowSign: GE - LowSignLabel: '>=' - HighSign: LE - HighSignLabel: <= - Low: 70 - High: 100 - AgeStart: 6570 - AgeEnd: 36135 - Flag: 'N' - Interpretation: Normal - TEST_threshold: - summary: Technical test with threshold reference (panic) - value: - TestSiteID: 2 - SiteID: 1 - TestSiteCode: GLU - TestSiteName: Glucose - TestType: TEST - DisciplineID: 2 - DepartmentID: 2 - ResultType: NMRIC - RefType: THOLD - Unit1: mg/dL - Decimal: 0 - Method: Hexokinase - SeqScr: 11 - SeqRpt: 11 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - refnum: - - RefNumID: 2 - NumRefType: THOLD - NumRefTypeLabel: Threshold - RangeType: PANIC - RangeTypeLabel: Panic Range - Sex: '1' - SexLabel: Female - LowSign: LT - LowSignLabel: < - High: 40 - AgeStart: 0 - AgeEnd: 43800 - Flag: L - Interpretation: Critical Low - TEST_text: - summary: Technical test with text reference - value: - TestSiteID: 3 - SiteID: 1 - TestSiteCode: STAGE - TestSiteName: Disease Stage - TestType: TEST - DisciplineID: 1 - DepartmentID: 1 - ResultType: VSET - RefType: TEXT - SeqScr: 50 - SeqRpt: 50 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - reftxt: - - RefTxtID: 1 - TxtRefType: TEXT - TxtRefTypeLabel: Text - Sex: '2' - SexLabel: Male - AgeStart: 6570 - AgeEnd: 36135 - RefTxt: NORM=Normal;HYPO=Hypochromic;MACRO=Macrocytic - Flag: 'N' - PARAM: - summary: Parameter test - value: - TestSiteID: 4 - SiteID: 1 - TestSiteCode: HEIGHT - TestSiteName: Height - TestType: PARAM - DisciplineID: 10 - ResultType: NMRIC - Unit1: cm - Decimal: 0 - SeqScr: 40 - SeqRpt: 40 - isVisibleScr: 1 - isVisibleRpt: 0 - isCountStat: 0 - CALC: - summary: Calculated test with reference - value: - TestSiteID: 5 - SiteID: 1 - TestSiteCode: EGFR - TestSiteName: eGFR - TestType: CALC - DisciplineID: 2 - DepartmentID: 2 - Unit1: mL/min/1.73m2 - Decimal: 0 - SeqScr: 20 - SeqRpt: 20 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 0 - testdefcal: - - TestCalID: 1 - DisciplineID: 2 - DepartmentID: 2 - FormulaCode: CKD_EPI(CREA,AGE,GENDER) - Unit1: mL/min/1.73m2 - Decimal: 0 - testdefgrp: - members: - - TestSiteID: 21 - TestSiteCode: CREA - TestSiteName: Creatinine - TestType: TEST - - TestSiteID: 51 - TestSiteCode: AGE - TestSiteName: Age - TestType: PARAM - refnum: - - RefNumID: 5 - NumRefType: NMRC - NumRefTypeLabel: Numeric - RangeType: REF - RangeTypeLabel: Reference Range - Sex: '1' - SexLabel: Female - LowSign: GE - LowSignLabel: '>=' - HighSign: LE - HighSignLabel: <= - Low: 10 - High: 20 - AgeStart: 6570 - AgeEnd: 43800 - Flag: 'N' - Interpretation: Normal - GROUP: - summary: Panel/Profile test - value: - TestSiteID: 6 - SiteID: 1 - TestSiteCode: LIPID - TestSiteName: Lipid Panel - TestType: GROUP - DisciplineID: 2 - DepartmentID: 2 - SeqScr: 51 - SeqRpt: 51 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 1 - testdefgrp: - members: - - TestGrpID: 1 - TestSiteID: 6 - Member: 100 - MemberTestSiteID: 100 - TestSiteCode: CHOL - TestSiteName: Total Cholesterol - TestType: TEST - - TestGrpID: 2 - TestSiteID: 6 - Member: 101 - MemberTestSiteID: 101 - TestSiteCode: TG - TestSiteName: Triglycerides - TestType: TEST - TITLE: - summary: Section header - value: - TestSiteID: 7 - SiteID: 1 - TestSiteCode: CHEM_HEADER - TestSiteName: '--- CHEMISTRY ---' - TestType: TITLE - DisciplineID: 2 - DepartmentID: 2 - SeqScr: 100 - SeqRpt: 100 - isVisibleScr: 1 - isVisibleRpt: 1 - isCountStat: 0 - TestMap: - type: object - properties: - TestMapID: - type: integer - HostType: - type: string - description: Host type code (e.g., SITE, WORKSTATION, INSTRUMENT) - HostID: - type: string - description: Host identifier - ClientType: - type: string - description: Client type code (e.g., SITE, WORKSTATION, INSTRUMENT) - ClientID: - type: string - description: Client identifier - HostName: - type: string - description: Resolved host name (from view) - ClientName: - type: string - description: Resolved client name (from view) - details: - type: array - description: Test mapping detail records - items: - $ref: '#/components/schemas/TestMapDetail' - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - description: Soft delete timestamp - OrderTestList: - type: object - properties: - InternalOID: - type: integer - description: Internal order ID - OrderID: - type: string - description: Order ID (e.g., 0025030300001) - PlacerID: - type: string - nullable: true - InternalPID: - type: integer - description: Patient internal ID - SiteID: - type: integer - PVADTID: - type: integer - description: Visit ADT ID - ReqApp: - type: string - nullable: true - Priority: - type: string - enum: - - R - - S - - U - description: | - R: Routine - S: Stat - U: Urgent - PriorityLabel: - type: string - description: Priority display text - TrnDate: - type: string - format: date-time - description: Transaction/Order date - EffDate: - type: string - format: date-time - description: Effective date - CreateDate: - type: string - format: date-time - OrderStatus: - type: string - enum: - - ORD - - SCH - - ANA - - VER - - REV - - REP - description: | - ORD: Ordered - SCH: Scheduled - ANA: Analysis - VER: Verified - REV: Reviewed - REP: Reported - OrderStatusLabel: - type: string - description: Order status display text - OrderTest: - type: object - properties: - InternalOID: - type: integer - description: Internal order ID - OrderID: - type: string - description: Order ID (e.g., 0025030300001) - PlacerID: - type: string - nullable: true - InternalPID: - type: integer - description: Patient internal ID - SiteID: - type: integer - PVADTID: - type: integer - description: Visit ADT ID - ReqApp: - type: string - nullable: true - Priority: - type: string - enum: - - R - - S - - U - description: | - R: Routine - S: Stat - U: Urgent - PriorityLabel: - type: string - description: Priority display text - TrnDate: - type: string - format: date-time - description: Transaction/Order date - EffDate: - type: string - format: date-time - description: Effective date - CreateDate: - type: string - format: date-time - OrderStatus: - type: string - enum: - - ORD - - SCH - - ANA - - VER - - REV - - REP - description: | - ORD: Ordered - SCH: Scheduled - ANA: Analysis - VER: Verified - REV: Reviewed - REP: Reported - OrderStatusLabel: - type: string - description: Order status display text - Specimens: - type: array - items: - $ref: '#/components/schemas/OrderSpecimen' - description: Associated specimens for this order - Tests: - type: array - items: - $ref: '#/components/schemas/OrderTestItem' - description: Test results (patres) for this order - OrderItem: - type: object - properties: - id: - type: integer - OrderID: - type: string - TestID: - type: integer - SpecimenID: - type: string - Status: - type: string - EdgeResultRequest: - type: object - required: - - sample_id - - instrument_id - properties: - sample_id: - type: string - description: Sample barcode/identifier - instrument_id: - type: string - description: Instrument identifier - patient_id: - type: string - description: Patient identifier (optional) - results: - type: array - items: - type: object - properties: - test_code: - type: string - result_value: - type: string - unit: - type: string - flags: - type: string - enum: - - H - - L - - 'N' - - A - description: H=High, L=Low, N=Normal, A=Abnormal - EdgeResultResponse: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Result received and queued - data: - type: object - properties: - edge_res_id: - type: integer - sample_id: - type: string - instrument_id: - type: string - EdgeOrder: - type: object - properties: - OrderID: - type: string - PatientID: - type: string - SampleID: - type: string - Tests: - type: array - items: - type: object - properties: - TestCode: - type: string - TestName: - type: string - SpecimenType: - type: string - Priority: - type: string - DueDateTime: - type: string - format: date-time - ValueSetLibItem: - type: object - description: Library/system value set item from JSON files - properties: - value: - type: string - description: The value/key code - label: - type: string - description: The display label - ValueSetDef: - type: object - description: User-defined value set definition (from database) - properties: - VSetID: - type: integer - description: Primary key - SiteID: - type: integer - description: Site reference - VSName: - type: string - description: Value set name - VSDesc: - type: string - description: Value set description - CreateDate: - type: string - format: date-time - description: Creation timestamp - EndDate: - type: string - format: date-time - nullable: true - description: Soft delete timestamp - ItemCount: - type: integer - description: Number of items in this value set - ValueSetItem: - type: object - description: User-defined value set item (from database) - properties: - VID: - type: integer - description: Primary key - SiteID: - type: integer - description: Site reference - VSetID: - type: integer - description: Reference to value set definition - VOrder: - type: integer - description: Display order - VValue: - type: string - description: The value code - VDesc: - type: string - description: The display description/label - VCategory: - type: string - description: Category code - CreateDate: - type: string - format: date-time - description: Creation timestamp - EndDate: - type: string - format: date-time - nullable: true - description: Soft delete timestamp - VSName: - type: string - description: Value set name (from joined definition) - Location: - type: object - properties: - LocationID: - type: integer - description: Primary key - SiteID: - type: integer - description: Reference to site - LocCode: - type: string - maxLength: 6 - description: Location code (short identifier) - Parent: - type: integer - nullable: true - description: Parent location ID for hierarchical locations - LocFull: - type: string - maxLength: 255 - description: Full location name - Description: - type: string - maxLength: 255 - description: Location description - LocType: - type: string - description: Location type code (e.g., ROOM, WARD, BUILDING) - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - nullable: true - EquipmentList: - type: object - properties: - EID: - type: integer - description: Equipment ID (auto-increment) - IEID: - type: string - maxLength: 50 - description: Internal Equipment ID - DepartmentID: - type: integer - description: Reference to department - InstrumentID: - type: string - maxLength: 150 - description: Instrument identifier - InstrumentName: - type: string - maxLength: 150 - description: Instrument display name - WorkstationID: - type: integer - description: Reference to workstation - Enable: - type: integer - enum: - - 0 - - 1 - description: Equipment status (0=disabled, 1=enabled) - EquipmentRole: - type: string - maxLength: 1 - description: Equipment role code - CreateDate: - type: string - format: date-time - description: Creation timestamp - EndDate: - type: string - format: date-time - nullable: true - description: Deletion timestamp (soft delete) - DepartmentName: - type: string - description: Joined department name - WorkstationName: - type: string - description: Joined workstation name - User: - type: object - properties: - UserID: - type: integer - description: Unique user identifier - Username: - type: string - description: Unique login username - Email: - type: string - format: email - description: User email address - Name: - type: string - description: Full name of the user - Role: - type: string - description: User role (admin, technician, doctor, etc.) - Department: - type: string - description: Department name - IsActive: - type: boolean - description: Whether the user account is active - CreatedAt: - type: string - format: date-time - description: Creation timestamp - UpdatedAt: - type: string - format: date-time - description: Last update timestamp - DelDate: - type: string - format: date-time - nullable: true - description: Soft delete timestamp (null if active) - UserCreate: - type: object - required: - - Username - - Email - properties: - Username: - type: string - minLength: 3 - maxLength: 50 - description: Unique login username - Email: - type: string - format: email - maxLength: 100 - description: User email address - Name: - type: string - description: Full name of the user - Role: - type: string - description: User role - Department: - type: string - description: Department name - IsActive: - type: boolean - default: true - description: Whether the user account is active - UserUpdate: - type: object - required: - - UserID - properties: - UserID: - type: integer - description: User ID to update - Email: - type: string - format: email - description: User email address - Name: - type: string - description: Full name of the user - Role: - type: string - description: User role - Department: - type: string - description: Department name - IsActive: - type: boolean - description: Whether the user account is active - UserListResponse: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Users retrieved successfully - data: - type: object - properties: - users: - type: array - items: - $ref: '#/components/schemas/User' - pagination: - type: object - properties: - current_page: - type: integer - per_page: - type: integer - total: - type: integer - total_pages: - type: integer - RuleDef: - type: object - properties: - RuleID: - type: integer - RuleCode: - type: string - example: AUTO_SET_RESULT - RuleName: - type: string - example: Automatically Set Result - Description: - type: string - nullable: true - EventCode: - type: string - example: ORDER_CREATED - ConditionExpr: - type: string - nullable: true - description: Raw DSL expression (editable) - example: if(sex('M'); result_set(0.5); result_set(0.6)) - ConditionExprCompiled: - type: string - nullable: true - description: Compiled JSON structure (auto-generated from ConditionExpr) - example: '{"conditionExpr":"patient[\"Sex\"] == \"M\"","valueExpr":"(patient[\"Sex\"] == \"M\") ? 0.5 : 0.6","then":[{"type":"RESULT_SET","value":0.5,"valueExpr":"0.5"}],"else":[{"type":"RESULT_SET","value":0.6,"valueExpr":"0.6"}]}' - CreateDate: - type: string - format: date-time - nullable: true - StartDate: - type: string - format: date-time - nullable: true - EndDate: - type: string - format: date-time - nullable: true - RuleWithDetails: - allOf: - - $ref: '#/components/schemas/RuleDef' - - type: object - properties: - linkedTests: - type: array - items: - type: integer - description: Array of TestSiteIDs this rule is linked to. Rules are active only when attached to tests. - TestRule: - type: object - description: Mapping between a rule and a test site (testrule table). Rules are active when linked via this table. - properties: - TestRuleID: - type: integer - RuleID: - type: integer - TestSiteID: - type: integer - CreateDate: - type: string - format: date-time - nullable: true - EndDate: - type: string - format: date-time - nullable: true - AuditLogEntry: - type: object - properties: - LogPatientID: - type: integer - nullable: true - LogOrderID: - type: integer - nullable: true - LogMasterID: - type: integer - nullable: true - LogSystemID: - type: integer - nullable: true - TblName: - type: string - RecID: - type: string - FldName: - type: string - nullable: true - FldValuePrev: - type: string - nullable: true - FldValueNew: - type: string - nullable: true - UserID: - type: string - SiteID: - type: string - DIDType: - type: string - nullable: true - DID: - type: string - nullable: true - MachineID: - type: string - nullable: true - SessionID: - type: string - AppID: - type: string - ProcessID: - type: string - nullable: true - WebPageID: - type: string - nullable: true - EventID: - type: string - ActivityID: - type: string - Reason: - type: string - nullable: true - LogDate: - type: string - format: date-time - Context: - type: string - IpAddress: - type: string - nullable: true - AuditLogListResponse: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/AuditLogEntry' - pagination: - type: object - properties: - page: - type: integer - perPage: - type: integer - total: - type: integer - required: - - page - - perPage - - total - required: - - data - - pagination - AuditLogsEnvelope: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/AuditLogListResponse' - required: - - status - - message - - data - AuditLogsErrorResponse: - type: object - properties: - status: - type: string - message: - type: string - data: - nullable: true - required: - - status - - message - - data - Contact: - type: object - properties: - ContactID: - type: integer - description: Primary key - NameFirst: - type: string - description: First name (required) - NameLast: - type: string - description: Last name - Title: - type: string - description: Title (e.g., Dr, Mr, Mrs) - Initial: - type: string - description: Middle initial - Birthdate: - type: string - format: date-time - description: Date of birth - EmailAddress1: - type: string - format: email - description: Primary email address - EmailAddress2: - type: string - format: email - description: Secondary email address - Phone: - type: string - description: Primary phone number - MobilePhone1: - type: string - description: Primary mobile number - MobilePhone2: - type: string - description: Secondary mobile number - Specialty: - type: string - description: Medical specialty code - SubSpecialty: - type: string - description: Sub-specialty code - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - description: Occupation display text - ContactDetail: - type: object - properties: - ContactDetID: - type: integer - description: Primary key - ContactID: - type: integer - description: Parent contact ID - SiteID: - type: integer - nullable: true - description: Site identifier - ContactCode: - type: string - nullable: true - description: Contact code at site - ContactEmail: - type: string - nullable: true - description: Contact email address - OccupationID: - type: integer - nullable: true - description: Occupation reference - JobTitle: - type: string - nullable: true - description: Job title - Department: - type: string - nullable: true - description: Department name - ContactStartDate: - type: string - format: date-time - ContactEndDate: - type: string - format: date-time - nullable: true - ContactDetailOperations: - type: object - properties: - created: - type: array - description: New contact details to create - items: - $ref: '#/components/schemas/ContactDetail' - edited: - type: array - description: Existing contact details to update - items: - allOf: - - $ref: '#/components/schemas/ContactDetail' - - type: object - required: - - ContactDetID - deleted: - type: array - description: Contact detail IDs to soft delete - items: - type: integer - OrderSpecimen: - type: object - properties: - InternalSID: - type: integer - description: Internal specimen ID - SID: - type: string - description: Specimen ID (e.g., 0025030300001-S01) - SiteID: - type: integer - OrderID: - type: integer - description: Reference to internal order ID - ConDefID: - type: integer - description: Container Definition ID - nullable: true - ConCode: - type: string - description: Container code - nullable: true - ConName: - type: string - description: Container name - nullable: true - Qty: - type: integer - description: Quantity - Unit: - type: string - description: Unit of measurement - Status: - type: string - enum: - - PENDING - - COLLECTED - - RECEIVED - - PREPARED - - REJECTED - description: Current specimen status - GenerateBy: - type: string - description: Source that generated this specimen - CreateDate: - type: string - format: date-time - OrderTestItem: - type: object - properties: - ResultID: - type: integer - description: Unique result ID - OrderID: - type: integer - description: Reference to internal order ID - InternalSID: - type: integer - description: Reference to specimen - nullable: true - TestSiteID: - type: integer - description: Test definition site ID - TestSiteCode: - type: string - description: Test code - TestSiteName: - type: string - description: Test name - nullable: true - TestType: - type: string - description: Test type code identifying the test category - enum: - - TEST - - PARAM - - CALC - - GROUP - - TITLE - SID: - type: string - description: Order ID reference - SampleID: - type: string - description: Sample ID (same as OrderID) - SeqScr: - type: integer - nullable: true - description: Sequence number for this test on the screen - SeqRpt: - type: integer - nullable: true - description: Sequence number for this test in reports - Result: - type: string - description: Test result value - nullable: true - Discipline: - type: object - description: Discipline metadata used for ordering tests - properties: - DisciplineID: - type: integer - nullable: true - DisciplineCode: - type: string - nullable: true - DisciplineName: - type: string - nullable: true - SeqScr: - type: integer - nullable: true - description: Discipline sequence on the screen - SeqRpt: - type: integer - nullable: true - description: Discipline sequence in reports - ResultDateTime: - type: string - format: date-time - description: Result timestamp - CreateDate: - type: string - format: date-time - PatientPatch: - type: object - description: | - Partial patient update payload. - Omitted fields are left unchanged. Send null explicitly to clear nullable fields. - properties: - PatientID: - type: string - maxLength: 30 - pattern: ^[A-Za-z0-9]+$ - description: Internal patient identifier - AlternatePID: - type: string - maxLength: 30 - pattern: ^[A-Za-z0-9]+$ - Prefix: - type: string - maxLength: 10 - enum: - - Mr - - Mrs - - Ms - - Dr - - Prof - Sex: - type: string - enum: - - '1' - - '2' - description: '1: Female, 2: Male' - NameFirst: - type: string - minLength: 1 - maxLength: 60 - pattern: ^[A-Za-z'\. ]+$ - NameMiddle: - type: string - minLength: 1 - maxLength: 60 - NameMaiden: - type: string - minLength: 1 - maxLength: 60 - NameLast: - type: string - minLength: 1 - maxLength: 60 - Suffix: - type: string - maxLength: 10 - Birthdate: - type: string - format: date-time - description: ISO 8601 UTC datetime - PlaceOfBirth: - type: string - maxLength: 100 - Citizenship: - type: string - maxLength: 100 - Street_1: - type: string - maxLength: 255 - Street_2: - type: string - maxLength: 255 - Street_3: - type: string - maxLength: 255 - ZIP: - type: string - maxLength: 10 - pattern: ^[0-9]+$ - Phone: - type: string - pattern: ^\+?[0-9]{8,15}$ - MobilePhone: - type: string - pattern: ^\+?[0-9]{8,15}$ - EmailAddress1: - type: string - format: email - maxLength: 100 - EmailAddress2: - type: string - format: email - maxLength: 100 - PatIdt: - allOf: - - $ref: '#/components/schemas/PatientIdentifier' - nullable: true - LinkTo: - type: array - description: Array of linked patient references - items: - $ref: '#/components/schemas/LinkedPatient' - Custodian: - allOf: - - $ref: '#/components/schemas/Custodian' - nullable: true - isDead: - type: string - enum: - - '0' - - '1' - description: '0: No (alive), 1: Yes (deceased)' - TimeOfDeath: - type: string - format: date-time - description: ISO 8601 UTC datetime of death - PatCom: - type: string - description: Patient comment/notes - nullable: true - PatAtt: - type: array - description: Patient address entries - nullable: true - items: - $ref: '#/components/schemas/PatAttEntry' - Province: - type: integer - description: Province AreaGeoID (foreign key to areageo table) - City: - type: integer - description: City AreaGeoID (foreign key to areageo table) - Country: - type: string - maxLength: 10 - description: Country ISO 3-letter code (e.g., IDN, USA) - Race: - type: string - maxLength: 100 - MaritalStatus: - type: string - enum: - - A - - B - - D - - M - - S - - W - description: 'A: Annulled, B: Separated, D: Divorced, M: Married, S: Single, W: Widowed' - Religion: - type: string - maxLength: 100 - Ethnic: - type: string - maxLength: 100 - TestMapDetail: - type: object - properties: - TestMapDetailID: - type: integer - TestMapID: - type: integer - HostTestCode: - type: string - description: Test code in host system - HostTestName: - type: string - description: Test name in host system - ConDefID: - type: integer - description: Container definition ID - ContainerLabel: - type: string - description: Container definition name - ClientTestCode: - type: string - description: Test code in client system - ClientTestName: - type: string - description: Test name in client system - CreateDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - description: Soft delete timestamp - TestDefinitionListItem: - type: object - properties: - TestSiteID: - type: integer - TestSiteCode: - type: string - TestSiteName: - type: string - TestType: - type: string - enum: - - TEST - - PARAM - - CALC - - GROUP - - TITLE - SeqScr: - type: integer - SeqRpt: - type: integer - isVisibleScr: - type: integer - enum: - - 0 - - 1 - isVisibleRpt: - type: integer - enum: - - 0 - - 1 - isCountStat: - type: integer - StartDate: - type: string - format: date-time - EndDate: - type: string - format: date-time - nullable: true - DisciplineID: - type: integer - nullable: true - DepartmentID: - type: integer - nullable: true - DisciplineName: - type: string - nullable: true - DepartmentName: - type: string - nullable: true - ValueSetListItem: - type: object - description: Library/system value set summary (from JSON files) - properties: - value: - type: string - description: The value set key/name - label: - type: string - description: The display name/label - count: - type: integer - description: Number of items in this value set +openapi: 3.1.0 +info: + title: CLQMS - Clinical Laboratory Quality Management System API + description: | + API for Clinical Laboratory Quality Management System supporting patient management, + specimen tracking, test ordering, instrument integration, and laboratory operations. + + **IMPORTANT:** For OpenAPI tools (Swagger UI, Redoc, Postman, etc.), use the bundled file: + `api-docs.bundled.yaml` which contains all paths and schemas merged into one file. + + This file (api-docs.yaml) contains schema references and is meant for development. + The paths are defined in separate files in the `paths/` directory. + version: 1.0.0 + contact: + name: CLQMS API Support + license: + name: Proprietary +servers: + - url: http://localhost/clqms01/ + description: Local development server + - url: https://clqms01-api.services-summit.my.id/ + description: Production server +tags: + - name: Authentication + description: User authentication and session management + - name: Patient + description: Patient registration and management + - name: Patient Visit + description: Patient visit/encounter management + - name: Organization + description: Organization structure (accounts, sites, disciplines, departments, workstations) + - name: Location + description: Location management (rooms, wards, buildings) + - name: Equipment + description: Laboratory equipment and instrument management + - name: Specimen + description: Specimen and container management + - name: Test + description: Test definitions and test catalog + - name: Rule + description: Rule engine - rules can be linked to multiple tests via testrule mapping table + - name: Calculation + description: Lightweight calculator endpoint for retrieving computed values by code or name + - name: Order + description: Laboratory order management + - name: Result + description: Patient results reporting with auto-validation + - name: Report + description: Lab report generation (HTML view) + - name: Edge API + description: Instrument integration endpoints + - name: Contact + description: Contact management (doctors, practitioners, etc.) + - name: ValueSet + description: Value set definitions and items + - name: User + description: User management and administration + - name: Demo + description: Demo/test endpoints (no authentication) + - name: Audit + description: Audit log retrieval and filtering +paths: + /api/audit-logs: + get: + tags: + - Audit + summary: Retrieve audit log entries for a table + security: + - bearerAuth: [] + parameters: + - name: table + in: query + required: true + schema: + type: string + description: Table alias for the audit data (logpatient, logorder, logmaster, logsystem) + - name: rec_id + in: query + schema: + type: string + description: Primary record identifier (RecID) to filter audit rows + - name: event_id + in: query + schema: + type: string + description: Canonical EventID (case insensitive) + - name: activity_id + in: query + schema: + type: string + description: Canonical ActivityID (case insensitive) + - name: from + in: query + schema: + type: string + format: date-time + description: Lower bound for LogDate inclusive + - name: to + in: query + schema: + type: string + format: date-time + description: Upper bound for LogDate inclusive + - name: search + in: query + schema: + type: string + description: Search term that matches user, reason, field names, or values + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number + - name: perPage + in: query + schema: + type: integer + default: 20 + description: Items per page (max 100) + responses: + '200': + description: Audit log results + content: + application/json: + schema: + $ref: '#/components/schemas/AuditLogsEnvelope' + '400': + description: Validation failure (missing table or invalid filters) + content: + application/json: + schema: + $ref: '#/components/schemas/AuditLogsErrorResponse' + '500': + description: Internal error when retrieving audit logs + content: + application/json: + schema: + $ref: '#/components/schemas/AuditLogsErrorResponse' + /api/auth/login: + post: + tags: + - Authentication + summary: User login + description: Authenticate user and receive JWT token via HTTP-only cookie + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Login successful + headers: + Set-Cookie: + description: JWT token in HTTP-only cookie + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '400': + description: Missing username + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/auth/logout: + post: + tags: + - Authentication + summary: User logout + description: Clear JWT token cookie + security: + - bearerAuth: [] + responses: + '200': + description: Logout successful + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/auth/check: + get: + tags: + - Authentication + summary: Check authentication status + security: + - bearerAuth: [] + - cookieAuth: [] + responses: + '200': + description: Authenticated + content: + application/json: + schema: + type: object + properties: + authenticated: + type: boolean + user: + type: object + '401': + description: Not authenticated + /api/auth/register: + post: + tags: + - Authentication + summary: Register new user + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + responses: + '201': + description: User created + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/auth/change_pass: + post: + tags: + - Authentication + summary: Change password + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - current_password + - new_password + properties: + current_password: + type: string + format: password + new_password: + type: string + format: password + responses: + '200': + description: Password changed successfully + /v2/auth/login: + post: + tags: + - Authentication + summary: V2 User login + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Login successful + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + /v2/auth/logout: + post: + tags: + - Authentication + summary: V2 User logout + responses: + '200': + description: Logout successful + /v2/auth/check: + get: + tags: + - Authentication + summary: V2 Check authentication + responses: + '200': + description: Auth check result + /v2/auth/register: + post: + tags: + - Authentication + summary: V2 Register new user + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + responses: + '201': + description: User created + /api/calc/testcode/{codeOrName}: + post: + tags: + - Calculation + summary: Evaluate a configured calculation by test code or name and return the raw result map. + security: [] + parameters: + - name: codeOrName + in: path + required: true + schema: + type: string + description: TestSiteCode or TestSiteName of the calculated test (case-insensitive). + requestBody: + required: true + content: + application/json: + schema: + type: object + description: Key-value pairs where keys match member tests used in the formula. + additionalProperties: + type: number + example: + TBIL: 5 + DBIL: 3 + responses: + '200': + description: Returns a single key/value pair with the canonical TestSiteCode or an empty object when the calculation is incomplete or missing. + content: + application/json: + schema: + type: object + examples: + success: + value: + 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: + - Contact + summary: List contacts + security: + - bearerAuth: [] + parameters: + - name: ContactName + in: query + schema: + type: string + description: Filter by contact name (searches in NameFirst and NameLast) + - name: Specialty + in: query + schema: + type: string + description: Filter by medical specialty code + responses: + '200': + description: List of contacts + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/Contact' + post: + tags: + - Contact + summary: Create new contact + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - NameFirst + properties: + NameFirst: + type: string + description: First name + NameLast: + type: string + description: Last name + Title: + type: string + description: Title (e.g., Dr, Mr, Mrs) + Initial: + type: string + description: Middle initial + Birthdate: + type: string + format: date-time + description: Date of birth + EmailAddress1: + type: string + format: email + description: Primary email address + EmailAddress2: + type: string + format: email + description: Secondary email address + Phone: + type: string + description: Primary phone number + MobilePhone1: + type: string + description: Primary mobile number + MobilePhone2: + type: string + description: Secondary mobile number + Specialty: + type: string + description: Medical specialty code + SubSpecialty: + type: string + description: Sub-specialty code + responses: + '201': + description: Contact created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Contact + summary: Delete contact + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - ContactID + properties: + ContactID: + type: integer + description: Contact ID to delete + responses: + '200': + description: Contact deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/contact/{id}: + get: + tags: + - Contact + summary: Get contact by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Contact ID + responses: + '200': + description: Contact details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/Contact' + patch: + tags: + - Contact + summary: Update contact + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Contact ID to update + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - NameFirst + properties: + NameFirst: + type: string + description: First name + NameLast: + type: string + description: Last name + Title: + type: string + description: Title (e.g., Dr, Mr, Mrs) + Initial: + type: string + description: Middle initial + Birthdate: + type: string + format: date-time + description: Date of birth + EmailAddress1: + type: string + format: email + description: Primary email address + EmailAddress2: + type: string + format: email + description: Secondary email address + Phone: + type: string + description: Primary phone number + MobilePhone1: + type: string + description: Primary mobile number + MobilePhone2: + type: string + description: Secondary mobile number + Specialty: + type: string + description: Medical specialty code + SubSpecialty: + type: string + description: Sub-specialty code + Details: + description: | + Detail payload supports either a flat array of new rows (legacy format) + or an operations object with `created`, `edited`, and `deleted` arrays. + oneOf: + - $ref: '#/components/schemas/ContactDetailOperations' + - type: array + description: Legacy format for replacing details with new rows only + items: + $ref: '#/components/schemas/ContactDetail' + responses: + '201': + description: Contact updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/demo/hello: + get: + tags: + - Demo + summary: Hello world endpoint + description: Simple test endpoint that returns a greeting message + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Hello, World! + /api/demo/ping: + get: + tags: + - Demo + summary: Ping endpoint + description: Health check endpoint to verify API is running + responses: + '200': + description: API is running + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: pong + timestamp: + type: string + format: date-time + /api/edge/result: + post: + tags: + - Edge API + summary: Receive results from instrument (tiny-edge) + description: | + Receives instrument results and stores them in the edgeres table for processing. + This endpoint is typically called by the tiny-edge middleware connected to laboratory analyzers. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EdgeResultRequest' + responses: + '201': + description: Result received and queued + content: + application/json: + schema: + $ref: '#/components/schemas/EdgeResultResponse' + '400': + description: Invalid JSON payload + /api/edge/order: + get: + tags: + - Edge API + summary: Fetch pending orders for instruments + description: Returns orders that need to be sent to laboratory instruments for testing + parameters: + - name: instrument_id + in: query + schema: + type: string + description: Filter by instrument + - name: status + in: query + schema: + type: string + enum: + - pending + - acknowledged + description: Filter by status + responses: + '200': + description: List of orders + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: array + items: + $ref: '#/components/schemas/EdgeOrder' + /api/edge/order/{orderId}/ack: + post: + tags: + - Edge API + summary: Acknowledge order delivery + description: Mark order as acknowledged by the instrument + parameters: + - name: orderId + in: path + required: true + schema: + type: integer + description: Edge order ID + responses: + '200': + description: Order acknowledged + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/edge/status: + post: + tags: + - Edge API + summary: Log instrument status update + description: Receive status updates from laboratory instruments + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - instrument_id + - status + properties: + instrument_id: + type: string + status: + type: string + enum: + - online + - offline + - error + - maintenance + message: + type: string + timestamp: + type: string + format: date-time + responses: + '200': + description: Status logged + /api/equipmentlist: + get: + tags: + - Equipment + summary: List equipment + description: Get list of equipment with optional filters + security: + - bearerAuth: [] + parameters: + - name: IEID + in: query + schema: + type: string + description: Filter by IEID + - name: InstrumentName + in: query + schema: + type: string + description: Filter by instrument name + - name: DepartmentID + in: query + schema: + type: integer + description: Filter by department ID + - name: WorkstationID + in: query + schema: + type: integer + description: Filter by workstation ID + - name: Enable + in: query + schema: + type: integer + enum: + - 0 + - 1 + description: Filter by enable status + responses: + '200': + description: List of equipment + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/EquipmentList' + post: + tags: + - Equipment + summary: Create equipment + description: Create a new equipment entry + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - IEID + - DepartmentID + - Enable + - EquipmentRole + properties: + IEID: + type: string + maxLength: 50 + DepartmentID: + type: integer + InstrumentID: + type: string + maxLength: 150 + InstrumentName: + type: string + maxLength: 150 + WorkstationID: + type: integer + Enable: + type: integer + enum: + - 0 + - 1 + EquipmentRole: + type: string + maxLength: 1 + responses: + '201': + description: Equipment created + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: integer + delete: + tags: + - Equipment + summary: Delete equipment + description: Soft delete an equipment entry + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - EID + properties: + EID: + type: integer + responses: + '200': + description: Equipment deleted + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + /api/equipmentlist/{id}: + get: + tags: + - Equipment + summary: Get equipment by ID + description: Get a single equipment entry by its EID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Equipment ID + responses: + '200': + description: Equipment details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/EquipmentList' + patch: + tags: + - Equipment + summary: Update equipment + description: Update an existing equipment entry + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Equipment ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + IEID: + type: string + maxLength: 50 + DepartmentID: + type: integer + InstrumentID: + type: string + maxLength: 150 + InstrumentName: + type: string + maxLength: 150 + WorkstationID: + type: integer + Enable: + type: integer + enum: + - 0 + - 1 + EquipmentRole: + type: string + maxLength: 1 + responses: + '200': + description: Equipment updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: integer + /api/location: + get: + tags: + - Location + summary: List locations + security: + - bearerAuth: [] + parameters: + - name: LocCode + in: query + schema: + type: string + description: Filter by location code + - name: LocName + in: query + schema: + type: string + description: Filter by location name (searches in LocFull) + responses: + '200': + description: List of locations + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/Location' + post: + tags: + - Location + summary: Create location + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - LocCode + - LocFull + properties: + SiteID: + type: integer + description: Reference to site + LocCode: + type: string + maxLength: 6 + description: Location code (short identifier) + Parent: + type: integer + nullable: true + description: Parent location ID for hierarchical locations + LocFull: + type: string + maxLength: 255 + description: Full location name + Description: + type: string + maxLength: 255 + description: Location description + LocType: + type: string + description: Location type code (e.g., ROOM, WARD, BUILDING) + responses: + '201': + description: Location created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Location + summary: Delete location + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - LocationID + properties: + LocationID: + type: integer + description: Location ID to delete + responses: + '200': + description: Location deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/location/{id}: + get: + tags: + - Location + summary: Get location by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Location ID + responses: + '200': + description: Location details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/Location' + patch: + tags: + - Location + summary: Update location + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Location ID to update + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + SiteID: + type: integer + description: Reference to site + LocCode: + type: string + maxLength: 6 + description: Location code (short identifier) + Parent: + type: integer + nullable: true + description: Parent location ID for hierarchical locations + LocFull: + type: string + maxLength: 255 + description: Full location name + Description: + type: string + maxLength: 255 + description: Location description + LocType: + type: string + description: Location type code (e.g., ROOM, WARD, BUILDING) + responses: + '201': + description: Location updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/ordertest: + get: + tags: + - Order + summary: List orders + security: + - bearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + - name: perPage + in: query + schema: + type: integer + - name: InternalPID + in: query + schema: + type: integer + description: Filter by internal patient ID + - name: OrderStatus + in: query + schema: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + description: | + ORD: Ordered + SCH: Scheduled + ANA: Analysis + VER: Verified + REV: Reviewed + REP: Reported + responses: + '200': + description: List of orders + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/OrderTestList' + post: + tags: + - Order + summary: Create order with specimens and tests + description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - InternalPID + - Tests + properties: + OrderID: + type: string + description: Optional custom order ID (auto-generated if not provided) + InternalPID: + type: integer + description: Patient internal ID + PatVisitID: + type: integer + description: Visit ID + SiteID: + type: integer + default: 1 + PlacerID: + type: string + Priority: + type: string + enum: + - R + - S + - U + default: R + description: | + R: Routine + S: Stat + U: Urgent + ReqApp: + type: string + description: Requesting application + Comment: + type: string + Tests: + type: array + items: + type: object + required: + - TestSiteID + properties: + TestSiteID: + type: integer + description: Test definition site ID + TestID: + type: integer + description: Alias for TestSiteID + responses: + '201': + description: Order created successfully with specimens and tests + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' + '400': + description: Validation error + '500': + description: Server error + delete: + tags: + - Order + summary: Delete order + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - OrderID + properties: + OrderID: + type: string + responses: + '200': + description: Order deleted + /api/ordertest/status: + post: + tags: + - Order + summary: Update order status + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - OrderID + - OrderStatus + properties: + OrderID: + type: string + OrderStatus: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + description: | + ORD: Ordered + SCH: Scheduled + ANA: Analysis + VER: Verified + REV: Reviewed + REP: Reported + responses: + '200': + description: Order status updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' + /api/ordertest/{id}: + get: + tags: + - Order + summary: Get order by ID + description: Returns order details with associated specimens and tests + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Order ID (e.g., 0025030300001) + responses: + '200': + description: Order details with specimens and tests + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' + patch: + tags: + - Order + summary: Update order + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Order ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + Priority: + type: string + enum: + - R + - S + - U + OrderStatus: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + OrderingProvider: + type: string + DepartmentID: + type: integer + WorkstationID: + type: integer + responses: + '200': + description: Order updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' + /api/organization/account: + get: + tags: + - Organization + summary: List accounts + security: + - bearerAuth: [] + responses: + '200': + description: List of accounts + post: + tags: + - Organization + summary: Create account + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + responses: + '201': + description: Account created + delete: + tags: + - Organization + summary: Delete account + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - AccountID + properties: + AccountID: + type: integer + responses: + '200': + description: Account deleted + /api/organization/account/{id}: + get: + tags: + - Organization + summary: Get account by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Account details + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + patch: + tags: + - Organization + summary: Update account + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + AccountName: + type: string + Parent: + type: integer + responses: + '200': + description: Account updated + /api/organization/site: + get: + tags: + - Organization + summary: List sites + security: + - bearerAuth: [] + responses: + '200': + description: List of sites + post: + tags: + - Organization + summary: Create site + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Site' + responses: + '201': + description: Site created + delete: + tags: + - Organization + summary: Delete site + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + type: integer + responses: + '200': + description: Site deleted + /api/organization/site/{id}: + get: + tags: + - Organization + summary: Get site by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Site details + patch: + tags: + - Organization + summary: Update site + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + SiteName: + type: string + SiteCode: + type: string + AccountID: + type: integer + responses: + '200': + description: Site updated + /api/organization/discipline: + get: + tags: + - Organization + summary: List disciplines + security: + - bearerAuth: [] + responses: + '200': + description: List of disciplines + post: + tags: + - Organization + summary: Create discipline + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Discipline' + responses: + '201': + description: Discipline created + delete: + tags: + - Organization + summary: Delete discipline + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + type: integer + responses: + '200': + description: Discipline deleted + /api/organization/discipline/{id}: + get: + tags: + - Organization + summary: Get discipline by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Discipline details + patch: + tags: + - Organization + summary: Update discipline + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + DisciplineName: + type: string + DisciplineCode: + type: string + SeqScr: + type: integer + description: Display order on screen + SeqRpt: + type: integer + description: Display order in reports + responses: + '200': + description: Discipline updated + /api/organization/department: + get: + tags: + - Organization + summary: List departments + security: + - bearerAuth: [] + responses: + '200': + description: List of departments + post: + tags: + - Organization + summary: Create department + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Department' + responses: + '201': + description: Department created + delete: + tags: + - Organization + summary: Delete department + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + type: integer + responses: + '200': + description: Department deleted + /api/organization/department/{id}: + get: + tags: + - Organization + summary: Get department by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Department details + patch: + tags: + - Organization + summary: Update department + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + DeptName: + type: string + DeptCode: + type: string + SiteID: + type: integer + responses: + '200': + description: Department updated + /api/organization/workstation: + get: + tags: + - Organization + summary: List workstations + security: + - bearerAuth: [] + responses: + '200': + description: List of workstations + post: + tags: + - Organization + summary: Create workstation + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Workstation' + responses: + '201': + description: Workstation created + delete: + tags: + - Organization + summary: Delete workstation + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + type: integer + responses: + '200': + description: Workstation deleted + /api/organization/workstation/{id}: + get: + tags: + - Organization + summary: Get workstation by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Workstation details + patch: + tags: + - Organization + summary: Update workstation + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + WorkstationName: + type: string + WorkstationCode: + type: string + SiteID: + type: integer + DepartmentID: + type: integer + responses: + '200': + description: Workstation updated + /api/organization/hostapp: + get: + tags: + - Organization + summary: List host applications + security: + - bearerAuth: [] + parameters: + - name: HostAppID + in: query + schema: + type: string + - name: HostAppName + in: query + schema: + type: string + responses: + '200': + description: List of host applications + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/HostApp' + post: + tags: + - Organization + summary: Create host application + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/HostApp' + responses: + '201': + description: Host application created + delete: + tags: + - Organization + summary: Delete host application (soft delete) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - HostAppID + properties: + HostAppID: + type: string + responses: + '200': + description: Host application deleted + /api/organization/hostapp/{id}: + get: + tags: + - Organization + summary: Get host application by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: Host application details + content: + application/json: + schema: + $ref: '#/components/schemas/HostApp' + patch: + tags: + - Organization + summary: Update host application + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + HostAppName: + type: string + SiteID: + type: integer + responses: + '200': + description: Host application updated + /api/organization/hostcompara: + get: + tags: + - Organization + summary: List host communication parameters + security: + - bearerAuth: [] + parameters: + - name: HostAppID + in: query + schema: + type: string + - name: HostIP + in: query + schema: + type: string + responses: + '200': + description: List of host communication parameters + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/HostComPara' + post: + tags: + - Organization + summary: Create host communication parameters + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/HostComPara' + responses: + '201': + description: Host communication parameters created + delete: + tags: + - Organization + summary: Delete host communication parameters (soft delete) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - HostAppID + properties: + HostAppID: + type: string + responses: + '200': + description: Host communication parameters deleted + /api/organization/hostcompara/{id}: + get: + tags: + - Organization + summary: Get host communication parameters by HostAppID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: Host communication parameters details + content: + application/json: + schema: + $ref: '#/components/schemas/HostComPara' + patch: + tags: + - Organization + summary: Update host communication parameters + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + HostIP: + type: string + HostPort: + type: string + HostPwd: + type: string + responses: + '200': + description: Host communication parameters updated + /api/organization/codingsys: + get: + tags: + - Organization + summary: List coding systems + security: + - bearerAuth: [] + parameters: + - name: CodingSysAbb + in: query + schema: + type: string + - name: FullText + in: query + schema: + type: string + responses: + '200': + description: List of coding systems + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/CodingSys' + post: + tags: + - Organization + summary: Create coding system + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CodingSys' + responses: + '201': + description: Coding system created + delete: + tags: + - Organization + summary: Delete coding system (soft delete) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - CodingSysID + properties: + CodingSysID: + type: integer + responses: + '200': + description: Coding system deleted + /api/organization/codingsys/{id}: + get: + tags: + - Organization + summary: Get coding system by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Coding system details + content: + application/json: + schema: + $ref: '#/components/schemas/CodingSys' + patch: + tags: + - Organization + summary: Update coding system + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + CodingSysAbb: + type: string + FullText: + type: string + Description: + type: string + responses: + '200': + description: Coding system updated + /api/patvisit: + get: + tags: + - Patient Visit + summary: List patient visits + security: + - bearerAuth: [] + parameters: + - name: InternalPID + in: query + schema: + type: integer + description: Filter by internal patient ID (exact match) + - name: PVID + in: query + schema: + type: string + description: Filter by visit ID (partial match) + - name: PatientID + in: query + schema: + type: string + description: Filter by patient ID (partial match) + - name: PatientName + in: query + schema: + type: string + description: Search by patient name (searches in both first and last name) + - name: CreateDateFrom + in: query + schema: + type: string + format: date-time + description: Filter visits created on or after this date + - name: CreateDateTo + in: query + schema: + type: string + format: date-time + description: Filter visits created on or before this date + - name: page + in: query + schema: + type: integer + - name: perPage + in: query + schema: + type: integer + responses: + '200': + description: List of patient visits + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/PatientVisit' + total: + type: integer + description: Total number of records + page: + type: integer + description: Current page number + per_page: + type: integer + description: Number of records per page + post: + tags: + - Patient Visit + summary: Create patient visit + description: | + Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided. + Can optionally include PatDiag (diagnosis) and PatVisitADT (ADT information). + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - InternalPID + properties: + PVID: + type: string + description: Visit ID (auto-generated with DV prefix if not provided) + InternalPID: + type: integer + description: Patient ID (required) + EpisodeID: + type: string + description: Episode identifier + SiteID: + type: integer + description: Site reference + PatDiag: + type: object + description: Optional diagnosis information + properties: + DiagCode: + type: string + Diagnosis: + type: string + PatVisitADT: + type: object + description: Optional ADT information + properties: + ADTCode: + type: string + enum: + - A01 + - A02 + - A03 + - A04 + - A08 + LocationID: + type: integer + AttDoc: + type: integer + RefDoc: + type: integer + AdmDoc: + type: integer + CnsDoc: + type: integer + responses: + '201': + description: Visit created successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: object + properties: + PVID: + type: string + InternalPVID: + type: integer + delete: + tags: + - Patient Visit + summary: Delete patient visit + security: + - bearerAuth: [] + responses: + '200': + description: Visit deleted successfully + /api/patvisit/{id}: + get: + tags: + - Patient Visit + summary: Get visit by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: PVID (visit identifier like DV00001) + responses: + '200': + description: Visit details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/PatientVisit' + patch: + tags: + - Patient Visit + summary: Update patient visit + description: | + Updates an existing patient visit. InternalPVID is required. + Can update main visit data, PatDiag, and add new PatVisitADT records. + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Internal visit ID (InternalPVID) + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + PVID: + type: string + InternalPID: + type: integer + EpisodeID: + type: string + SiteID: + type: integer + PatDiag: + type: object + description: Diagnosis information (will update if exists) + properties: + DiagCode: + type: string + Diagnosis: + type: string + PatVisitADT: + type: array + description: Array of ADT records to add (new records only) + items: + type: object + properties: + ADTCode: + type: string + enum: + - A01 + - A02 + - A03 + - A04 + - A08 + LocationID: + type: integer + AttDoc: + type: integer + RefDoc: + type: integer + AdmDoc: + type: integer + CnsDoc: + type: integer + sequence: + type: integer + description: Used for ordering multiple ADT records + responses: + '200': + description: Visit updated successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: object + properties: + PVID: + type: string + InternalPVID: + type: integer + /api/patvisit/patient/{patientId}: + get: + tags: + - Patient Visit + summary: Get visits by patient ID + security: + - bearerAuth: [] + parameters: + - name: patientId + in: path + required: true + schema: + type: integer + description: Internal Patient ID (InternalPID) + responses: + '200': + description: Patient visits list + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: array + items: + $ref: '#/components/schemas/PatientVisit' + /api/patvisitadt: + post: + tags: + - Patient Visit + summary: Create ADT record + description: Create a new Admission/Discharge/Transfer record + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PatVisitADT' + responses: + '201': + description: ADT record created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + delete: + tags: + - Patient Visit + summary: Delete ADT visit (soft delete) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - PVADTID + properties: + PVADTID: + type: integer + description: ADT record ID to delete + responses: + '200': + description: ADT visit deleted successfully + /api/patvisitadt/visit/{visitId}: + get: + tags: + - Patient Visit + summary: Get ADT history by visit ID + description: Retrieve the complete Admission/Discharge/Transfer history for a visit, including all locations and doctors + security: + - bearerAuth: [] + parameters: + - name: visitId + in: path + required: true + schema: + type: integer + description: Internal Visit ID (InternalPVID) + responses: + '200': + description: ADT history retrieved successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: ADT history retrieved + data: + type: array + items: + type: object + properties: + PVADTID: + type: integer + InternalPVID: + type: integer + ADTCode: + type: string + enum: + - A01 + - A02 + - A03 + - A04 + - A08 + LocationID: + type: integer + LocationName: + type: string + AttDoc: + type: integer + AttDocFirstName: + type: string + AttDocLastName: + type: string + RefDoc: + type: integer + RefDocFirstName: + type: string + RefDocLastName: + type: string + AdmDoc: + type: integer + AdmDocFirstName: + type: string + AdmDocLastName: + type: string + CnsDoc: + type: integer + CnsDocFirstName: + type: string + CnsDocLastName: + type: string + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + /api/patvisitadt/{id}: + get: + tags: + - Patient Visit + summary: Get ADT record by ID + description: Retrieve a single ADT record by its ID, including location and doctor details + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: ADT record ID (PVADTID) + responses: + '200': + description: ADT record retrieved successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: ADT record retrieved + data: + type: object + properties: + PVADTID: + type: integer + InternalPVID: + type: integer + ADTCode: + type: string + enum: + - A01 + - A02 + - A03 + - A04 + - A08 + LocationID: + type: integer + LocationName: + type: string + AttDoc: + type: integer + AttDocFirstName: + type: string + AttDocLastName: + type: string + RefDoc: + type: integer + RefDocFirstName: + type: string + RefDocLastName: + type: string + AdmDoc: + type: integer + AdmDocFirstName: + type: string + AdmDocLastName: + type: string + CnsDoc: + type: integer + CnsDocFirstName: + type: string + CnsDocLastName: + type: string + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + patch: + tags: + - Patient Visit + summary: Update ADT record + description: Update an existing ADT record + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: ADT record ID (PVADTID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PatVisitADT' + responses: + '200': + description: ADT record updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/patient: + get: + tags: + - Patient + summary: List patients + security: + - bearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + - name: perPage + in: query + schema: + type: integer + default: 20 + - name: InternalPID + in: query + schema: + type: integer + description: Filter by internal patient ID + - name: PatientID + in: query + schema: + type: string + description: Filter by patient ID + - name: Name + in: query + schema: + type: string + description: Search by patient name + - name: Birthdate + in: query + schema: + type: string + format: date + description: Filter by birthdate (YYYY-MM-DD) + responses: + '200': + description: List of patients + content: + application/json: + schema: + $ref: '#/components/schemas/PatientListResponse' + post: + tags: + - Patient + summary: Create new patient + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Patient' + responses: + '201': + description: Patient created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Patient + summary: Delete patient (soft delete) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - InternalPID + properties: + InternalPID: + type: integer + description: Internal patient record ID + responses: + '200': + description: Patient deleted successfully + /api/patient/check: + get: + tags: + - Patient + summary: Check if patient exists + security: + - bearerAuth: [] + parameters: + - name: PatientID + in: query + schema: + type: string + description: Patient ID to check + - name: EmailAddress + in: query + schema: + type: string + format: email + description: Email address to check + - name: Phone + in: query + schema: + type: string + description: Phone number to check + responses: + '200': + description: Patient check result + content: + application/json: + schema: + type: object + properties: + exists: + type: boolean + data: + $ref: '#/components/schemas/Patient' + /api/patient/{id}: + get: + tags: + - Patient + summary: Get patient by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Internal patient record ID + responses: + '200': + description: Patient details + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + $ref: '#/components/schemas/Patient' + patch: + tags: + - Patient + summary: Partially update patient + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Internal patient record ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PatientPatch' + responses: + '200': + description: Patient updated successfully + '400': + description: Validation error + '404': + description: Patient not found + /api/report/{orderID}: + get: + tags: + - Report + summary: Generate lab report + description: Generate an HTML lab report for a specific order. Returns HTML content that can be viewed in browser or printed to PDF. + security: + - bearerAuth: [] + parameters: + - name: orderID + in: path + required: true + schema: + type: integer + description: Internal Order ID + responses: + '200': + description: HTML lab report + content: + text/html: + schema: + type: string + description: HTML content of the lab report + '404': + description: Order or patient not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Failed to generate report + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/result: + get: + tags: + - Result + summary: List results + description: Retrieve patient test results with optional filters by order or patient + security: + - bearerAuth: [] + parameters: + - name: order_id + in: query + schema: + type: integer + description: Filter by internal order ID + - name: patient_id + in: query + schema: + type: integer + description: Filter by internal patient ID (returns cumulative results) + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number for pagination + - name: per_page + in: query + schema: + type: integer + default: 20 + description: Number of results per page + responses: + '200': + description: List of results + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: array + items: + type: object + properties: + ResultID: + type: integer + OrderID: + type: integer + TestSiteID: + type: integer + TestSiteCode: + type: string + Result: + type: string + nullable: true + ResultDateTime: + type: string + format: date-time + RefNumID: + type: integer + nullable: true + RefTxtID: + type: integer + nullable: true + CreateDate: + type: string + format: date-time + TestSiteName: + type: string + nullable: true + Unit1: + type: string + nullable: true + Unit2: + type: string + nullable: true + Low: + type: number + nullable: true + High: + type: number + nullable: true + LowSign: + type: string + nullable: true + HighSign: + type: string + nullable: true + RefDisplay: + type: string + nullable: true + /api/result/{id}: + get: + tags: + - Result + summary: Get result by ID + description: Retrieve a specific result entry with all related data + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Result ID + responses: + '200': + description: Result details + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + ResultID: + type: integer + SiteID: + type: integer + OrderID: + type: integer + InternalSID: + type: integer + SID: + type: string + SampleID: + type: string + TestSiteID: + type: integer + TestSiteCode: + type: string + AspCnt: + type: integer + Result: + type: string + nullable: true + SampleType: + type: string + nullable: true + ResultDateTime: + type: string + format: date-time + WorkstationID: + type: integer + nullable: true + EquipmentID: + type: integer + nullable: true + RefNumID: + type: integer + nullable: true + RefTxtID: + type: integer + nullable: true + CreateDate: + type: string + format: date-time + TestSiteName: + type: string + nullable: true + Unit1: + type: string + nullable: true + Unit2: + type: string + nullable: true + Low: + type: number + nullable: true + High: + type: number + nullable: true + LowSign: + type: string + nullable: true + HighSign: + type: string + nullable: true + RefDisplay: + type: string + nullable: true + OrderNumber: + type: string + nullable: true + InternalPID: + type: integer + '404': + description: Result not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + patch: + tags: + - Result + summary: Update result + description: Update a result value with automatic validation against reference ranges. Returns calculated flag (L/H) in response but does not store it. + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Result ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + Result: + type: string + description: The result value + RefNumID: + type: integer + description: Reference range ID to validate against + SampleType: + type: string + nullable: true + WorkstationID: + type: integer + nullable: true + EquipmentID: + type: integer + nullable: true + responses: + '200': + description: Result updated successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + result: + type: object + flag: + type: string + nullable: true + enum: + - L + - H + description: Calculated flag - L for Low, H for High, null for normal + '400': + description: Validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Result not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Result + summary: Delete result + description: Soft delete a result entry by setting DelDate + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Result ID + responses: + '200': + description: Result deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + '404': + description: Result not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/rule: + get: + tags: + - Rule + summary: List rules + security: + - bearerAuth: [] + parameters: + - name: EventCode + in: query + schema: + type: string + description: Filter by event code + - name: TestSiteID + in: query + schema: + type: integer + description: Filter by TestSiteID (returns rules linked to this test). Rules are only returned when attached to tests. + - name: search + in: query + schema: + type: string + description: Search by rule code or name + responses: + '200': + description: List of rules + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/RuleDef' + post: + tags: + - Rule + summary: Create rule + description: | + Create a new rule. Rules must be linked to at least one test via TestSiteIDs. + A single rule can be linked to multiple tests. Rules are active only when attached to tests. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + RuleCode: + type: string + example: AUTO_SET_RESULT + RuleName: + type: string + example: Automatically Set Result + Description: + type: string + EventCode: + type: string + example: test_created + TestSiteIDs: + type: array + items: + type: integer + description: Array of TestSiteIDs to link this rule to (required) + example: + - 1 + - 2 + - 3 + ConditionExpr: + type: string + nullable: true + description: Raw DSL expression. Compile it first and persist the compiled JSON in ConditionExprCompiled. + example: if(sex('M'); result_set(0.5); result_set(0.6)) + ConditionExprCompiled: + type: string + nullable: true + description: Compiled JSON payload from POST /api/rule/compile + required: + - RuleCode + - RuleName + - EventCode + - TestSiteIDs + responses: + '201': + description: Rule created + /api/rule/{id}: + get: + tags: + - Rule + summary: Get rule with linked tests + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: RuleID + responses: + '200': + description: Rule details with linked test sites + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/RuleWithDetails' + '404': + description: Rule not found + patch: + tags: + - Rule + summary: Update rule + description: | + Update a rule. TestSiteIDs can be provided to update which tests the rule is linked to. + Tests not in the new list will be unlinked, and new tests will be linked. + Rules are active only when attached to tests. + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: RuleID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + RuleCode: + type: string + RuleName: + type: string + Description: + type: string + EventCode: + type: string + TestSiteIDs: + type: array + items: + type: integer + description: Array of TestSiteIDs to link this rule to + ConditionExpr: + type: string + nullable: true + responses: + '200': + description: Rule updated + '404': + description: Rule not found + delete: + tags: + - Rule + summary: Soft delete rule + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: RuleID + responses: + '200': + description: Rule deleted + '404': + description: Rule not found + /api/rule/validate: + post: + tags: + - Rule + summary: Validate/evaluate an expression + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + expr: + type: string + context: + type: object + additionalProperties: true + required: + - expr + responses: + '200': + description: Validation result + /api/rule/compile: + post: + tags: + - Rule + summary: Compile DSL expression to engine-compatible structure + description: | + Compile a DSL expression to the engine-compatible JSON structure. + Frontend calls this when user clicks "Compile" button. + Returns compiled structure that can be saved to ConditionExprCompiled field. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + expr: + type: string + description: Raw DSL expression + example: if(sex('M'); result_set(0.5); result_set(0.6)) + required: + - expr + responses: + '200': + description: Compilation successful + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + data: + type: object + properties: + raw: + type: string + description: Original DSL expression + compiled: + type: object + description: Parsed structure with conditionExpr, valueExpr, then, else + conditionExprCompiled: + type: string + description: JSON string to save to ConditionExprCompiled field + '400': + description: Compilation failed (invalid syntax) + /api/specimen: + get: + tags: + - Specimen + summary: List specimens + security: + - bearerAuth: [] + responses: + '200': + description: List of specimens + post: + tags: + - Specimen + summary: Create specimen + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Specimen' + responses: + '201': + description: Specimen created + /api/specimen/{id}: + get: + tags: + - Specimen + summary: Get specimen by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Specimen details + patch: + tags: + - Specimen + summary: Update specimen + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen ID (SID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Specimen' + responses: + '200': + description: Specimen updated + delete: + tags: + - Specimen + summary: Delete specimen (soft delete) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen ID (SID) + responses: + '200': + description: Specimen deleted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Specimen deleted successfully + data: + type: object + properties: + SID: + type: integer + '404': + description: Specimen not found + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: Specimen not found + data: + type: null + '500': + description: Server error + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: Failed to delete specimen + data: + type: null + /api/specimen/container: + get: + tags: + - Specimen + summary: List container definitions + security: + - bearerAuth: [] + responses: + '200': + description: List of container definitions + post: + tags: + - Specimen + summary: Create container definition + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerDef' + responses: + '201': + description: Container definition created + /api/specimen/container/{id}: + get: + tags: + - Specimen + summary: Get container definition by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Container definition details + patch: + tags: + - Specimen + summary: Update container definition + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Container definition ID (ConDefID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerDef' + responses: + '200': + description: Container definition updated + /api/specimen/containerdef: + get: + tags: + - Specimen + summary: List container definitions (alias) + security: + - bearerAuth: [] + responses: + '200': + description: List of container definitions + post: + tags: + - Specimen + summary: Create container definition (alias) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerDef' + responses: + '201': + description: Container definition created + /api/specimen/containerdef/{id}: + patch: + tags: + - Specimen + summary: Update container definition (alias) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Container definition ID (ConDefID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerDef' + responses: + '200': + description: Container definition updated + /api/specimen/prep: + get: + tags: + - Specimen + summary: List specimen preparations + security: + - bearerAuth: [] + responses: + '200': + description: List of specimen preparations + post: + tags: + - Specimen + summary: Create specimen preparation + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenPrep' + responses: + '201': + description: Specimen preparation created + /api/specimen/prep/{id}: + get: + tags: + - Specimen + summary: Get specimen preparation by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Specimen preparation details + patch: + tags: + - Specimen + summary: Update specimen preparation + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen preparation ID (SpcPrpID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenPrep' + responses: + '200': + description: Specimen preparation updated + /api/specimen/status: + get: + tags: + - Specimen + summary: List specimen statuses + security: + - bearerAuth: [] + responses: + '200': + description: List of specimen statuses + post: + tags: + - Specimen + summary: Create specimen status + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenStatus' + responses: + '201': + description: Specimen status created + /api/specimen/status/{id}: + get: + tags: + - Specimen + summary: Get specimen status by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Specimen status details + patch: + tags: + - Specimen + summary: Update specimen status + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen status ID (SpcStaID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenStatus' + responses: + '200': + description: Specimen status updated + /api/specimen/collection: + get: + tags: + - Specimen + summary: List specimen collection methods + security: + - bearerAuth: [] + responses: + '200': + description: List of collection methods + post: + tags: + - Specimen + summary: Create specimen collection method + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenCollection' + responses: + '201': + description: Collection method created + /api/specimen/collection/{id}: + get: + tags: + - Specimen + summary: Get specimen collection method by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Collection method details + patch: + tags: + - Specimen + summary: Update specimen collection method + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen collection ID (SpcColID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenCollection' + responses: + '200': + description: Collection method updated + /api/test/testmap: + get: + tags: + - Test + summary: List all test mappings + security: + - bearerAuth: [] + parameters: + - name: host + in: query + required: false + schema: + type: string + description: Filter by host name, type, or ID + - name: client + in: query + required: false + schema: + type: string + description: Filter by client name, type, or ID + responses: + '200': + description: List of test mappings + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: array + items: + type: object + properties: + TestMapID: + type: integer + HostType: + type: string + HostID: + type: string + HostName: + type: string + ClientType: + type: string + ClientID: + type: string + ClientName: + type: string + post: + tags: + - Test + summary: Create test mapping (header only) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + HostType: + type: string + description: Host type code + HostID: + type: string + description: Host identifier + ClientType: + type: string + description: Client type code + ClientID: + type: string + description: Client identifier + details: + type: array + description: Optional detail records to create alongside the header + items: + type: object + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '201': + description: Test mapping created + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Created TestMapID + delete: + tags: + - Test + summary: Soft delete test mapping (cascades to details) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + description: Test Map ID to delete (required) + required: + - TestMapID + responses: + '200': + description: Test mapping deleted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Deleted TestMapID + '404': + description: Test mapping not found or already deleted + /api/test/testmap/{id}: + get: + tags: + - Test + summary: Get test mapping by ID with details + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Map ID + responses: + '200': + description: Test mapping details with nested detail records + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/TestMap' + '404': + description: Test mapping not found + patch: + tags: + - Test + summary: Update test mapping + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Map ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + HostType: + type: string + HostID: + type: string + ClientType: + type: string + ClientID: + type: string + details: + description: | + Detail payload supports either a flat array/object (treated as new rows) + or an operations object with `created`, `edited`, and `deleted` arrays. + oneOf: + - type: object + properties: + created: + type: array + description: New detail records to insert + items: + type: object + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + edited: + type: array + description: Existing detail records to update + items: + type: object + properties: + TestMapDetailID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + deleted: + type: array + description: TestMapDetailIDs to soft delete + items: + type: integer + - type: array + description: Shortcut format for creating new details only + items: + type: object + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + - type: object + description: Shortcut format for creating a single new detail + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Test mapping updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Updated TestMapID + /api/test/testmap/by-testcode/{testCode}: + get: + tags: + - Test + summary: Get test mappings by test code with details + security: + - bearerAuth: [] + parameters: + - name: testCode + in: path + required: true + schema: + type: string + description: Test Code (matches HostTestCode or ClientTestCode) + responses: + '200': + description: List of test mappings with details for the test code + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestMap' + /api/test/testmap/detail: + get: + tags: + - Test + summary: List test mapping details + security: + - bearerAuth: [] + parameters: + - name: TestMapID + in: query + schema: + type: integer + description: Filter by TestMapID + responses: + '200': + description: List of test mapping details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestMapDetail' + post: + tags: + - Test + summary: Create test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + description: Test Map ID (required) + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + required: + - TestMapID + responses: + '201': + description: Test mapping detail created + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: integer + description: Created TestMapDetailID + delete: + tags: + - Test + summary: Soft delete test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapDetailID: + type: integer + description: Test Map Detail ID to delete (required) + required: + - TestMapDetailID + responses: + '200': + description: Test mapping detail deleted + /api/test/testmap/detail/{id}: + get: + tags: + - Test + summary: Get test mapping detail by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Map Detail ID + responses: + '200': + description: Test mapping detail + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/TestMapDetail' + patch: + tags: + - Test + summary: Update test mapping detail + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Map Detail ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Test mapping detail updated + /api/test/testmap/detail/by-testmap/{testMapID}: + get: + tags: + - Test + summary: Get test mapping details by test map ID + security: + - bearerAuth: [] + parameters: + - name: testMapID + in: path + required: true + schema: + type: integer + description: Test Map ID + responses: + '200': + description: List of test mapping details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestMapDetail' + /api/test/testmap/detail/batch: + post: + tags: + - Test + summary: Batch create test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Batch create results + patch: + tags: + - Test + summary: Batch update test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + TestMapDetailID: + type: integer + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Batch update results + delete: + tags: + - Test + summary: Batch delete test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: integer + description: TestMapDetailIDs to delete + responses: + '200': + description: Batch delete results + /api/test: + get: + tags: + - Test + summary: List test definitions + security: + - bearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number for pagination + - name: perPage + in: query + schema: + type: integer + default: 20 + description: Number of items per page + - name: SiteID + in: query + schema: + type: integer + description: Filter by site ID + - name: TestType + in: query + schema: + type: string + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + description: Filter by test type + - name: isVisibleScr + in: query + schema: + type: integer + enum: + - 0 + - 1 + description: Filter by screen visibility (0=hidden, 1=visible) + - name: isVisibleRpt + in: query + schema: + type: integer + enum: + - 0 + - 1 + description: Filter by report visibility (0=hidden, 1=visible) + - name: search + in: query + schema: + type: string + description: Search by test code or name + responses: + '200': + description: List of test definitions + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestDefinitionListItem' + pagination: + type: object + properties: + total: + type: integer + description: Total number of records matching the query + examples: + list_flat: + summary: Flat list response from testdefsite + value: + status: success + message: Data fetched successfully + data: + - TestSiteID: 21 + TestSiteCode: GLU + TestSiteName: Glucose + TestType: TEST + SeqScr: 11 + SeqRpt: 11 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + StartDate: '2026-01-01 00:00:00' + EndDate: null + DisciplineID: 2 + DepartmentID: 2 + DisciplineName: Clinical Chemistry + DepartmentName: Laboratory + - TestSiteID: 22 + TestSiteCode: CREA + TestSiteName: Creatinine + TestType: TEST + SeqScr: 12 + SeqRpt: 12 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + StartDate: '2026-01-01 00:00:00' + EndDate: null + DisciplineID: 2 + DepartmentID: 2 + DisciplineName: Clinical Chemistry + DepartmentName: Laboratory + post: + tags: + - Test + summary: Create test definition + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + SiteID: + type: integer + description: Site ID (required) + TestSiteCode: + type: string + description: Test code (required) + TestSiteName: + type: string + description: Test name (required) + TestType: + type: string + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + description: Test type (required) + Description: + type: string + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: + - NMRIC + - RANGE + - TEXT + - VSET + - NORES + RefType: + type: string + enum: + - RANGE + - THOLD + - VSET + - TEXT + - NOREF + VSet: + type: integer + ReqQty: + type: number + format: decimal + ReqQtyUnit: + type: string + Unit1: + type: string + Factor: + type: number + format: decimal + Unit2: + type: string + Decimal: + type: integer + CollReq: + type: string + Method: + type: string + ExpectedTAT: + type: integer + SeqScr: + type: integer + SeqRpt: + type: integer + IndentLeft: + type: integer + FontStyle: + type: string + isVisibleScr: + type: integer + isVisibleRpt: + type: integer + isCountStat: + type: integer + testdefcal: + type: object + description: Calculated test metadata persisted in the `testdefcal` table. + properties: + FormulaCode: + type: string + description: Formula expression for calculated tests (e.g., "{TBIL} - {DBIL}") + testdefgrp: + type: object + description: Group definition wrapper for CALC/GROUP member assignments. + properties: + members: + type: array + description: Array of member TestSiteIDs for CALC/GROUP definitions. + items: + type: object + properties: + TestSiteID: + type: integer + description: Foreign key referencing the member test's TestSiteID. + required: + - TestSiteID + refnum: + type: array + items: + type: object + reftxt: + type: array + items: + type: object + testmap: + type: array + items: + type: object + properties: + HostType: + type: string + HostID: + type: string + HostTestCode: + type: string + HostTestName: + type: string + ClientType: + type: string + ClientID: + type: string + ClientTestCode: + type: string + ClientTestName: + type: string + ConDefID: + type: integer + nullable: true + required: + - SiteID + - TestSiteCode + - TestSiteName + - TestType + examples: + TEST_no_ref: + summary: Technical test without reference or map + value: + SiteID: 1 + TestSiteCode: TEST_NREF + TestSiteName: Numeric Test + TestType: TEST + SeqScr: 500 + SeqRpt: 500 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + DisciplineID: 2 + DepartmentID: 2 + Unit1: mg/dL + Method: CBC Analyzer + PARAM_no_ref: + summary: Parameter without reference or map + value: + SiteID: 1 + TestSiteCode: PARAM_NRF + TestSiteName: Clinical Parameter + TestType: PARAM + SeqScr: 10 + SeqRpt: 10 + isVisibleScr: 1 + isVisibleRpt: 0 + isCountStat: 0 + DisciplineID: 10 + DepartmentID: 0 + Unit1: cm + Method: Manual entry + TEST_range_single: + summary: Technical test with numeric range reference (single) + value: + SiteID: 1 + TestSiteCode: TEST_RANGE + TestSiteName: Glucose Range + TestType: TEST + SeqScr: 105 + SeqRpt: 105 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 6570 + AgeEnd: 36135 + Flag: 'N' + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + TEST_range_multiple_map: + summary: Numeric reference with multiple ranges and test map + value: + SiteID: 1 + TestSiteCode: TEST_RMAP + TestSiteName: Glucose Panic Range + TestType: TEST + SeqScr: 110 + SeqRpt: 110 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 6570 + AgeEnd: 36135 + Flag: 'N' + - NumRefType: NMRC + RangeType: REF + Sex: '1' + LowSign: '>' + Low: 75 + HighSign: < + High: 105 + AgeStart: 6570 + AgeEnd: 36135 + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + HostTestCode: GLU + HostTestName: Glucose + ClientType: WST + ClientID: '1' + ClientTestCode: GLU_C + ClientTestName: Glucose Client + ConDefID: 1 + - HostType: SITE + HostID: '1' + HostTestCode: CREA + HostTestName: Creatinine + ClientType: WST + ClientID: '1' + ClientTestCode: CREA_C + ClientTestName: Creatinine Client + ConDefID: 2 + - HostType: WST + HostID: '3' + HostTestCode: HB + HostTestName: Hemoglobin + ClientType: INST + ClientID: '2' + ClientTestCode: HB_C + ClientTestName: Hemoglobin Client + ConDefID: 3 + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + TEST_threshold: + summary: Technical test with threshold reference + value: + SiteID: 1 + TestSiteCode: TEST_THLD + TestSiteName: Sodium Threshold + TestType: TEST + SeqScr: 115 + SeqRpt: 115 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 45625 + Flag: H + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + TEST_threshold_map: + summary: Threshold reference plus test map + value: + SiteID: 1 + TestSiteCode: TEST_TMAP + TestSiteName: Potassium Panic + TestType: TEST + SeqScr: 120 + SeqRpt: 120 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 45625 + Flag: H + - NumRefType: THOLD + RangeType: PANIC + Sex: '1' + LowSign: < + Low: 121 + AgeStart: 0 + AgeEnd: 45625 + Flag: H + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '1' + details: + - HostTestCode: HB + HostTestName: Hemoglobin + ConDefID: 3 + ClientTestCode: HB_C + ClientTestName: Hemoglobin Client + - HostTestCode: GLU + HostTestName: Glucose + ConDefID: 1 + ClientTestCode: GLU_C + ClientTestName: Glucose Client + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + TEST_text: + summary: Technical test with text reference + value: + SiteID: 1 + TestSiteCode: TEST_TEXT + TestSiteName: Disease Stage + TestType: TEST + SeqScr: 130 + SeqRpt: 130 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 6570 + AgeEnd: 36135 + RefTxt: NORM=Normal;HIGH=High + Flag: 'N' + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + Method: Morphology + TEST_text_map: + summary: Text reference plus test map + value: + SiteID: 1 + TestSiteCode: TEST_TXM + TestSiteName: Disease Stage (Map) + TestType: TEST + SeqScr: 135 + SeqRpt: 135 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 6570 + AgeEnd: 36135 + RefTxt: NORM=Normal + Flag: 'N' + - SpcType: GEN + TxtRefType: TEXT + Sex: '1' + AgeStart: 6570 + AgeEnd: 36135 + RefTxt: ABN=Abnormal + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '1' + details: + - HostTestCode: STAGE + HostTestName: Disease Stage + ConDefID: 4 + ClientTestCode: STAGE_C + ClientTestName: Disease Stage Client + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + TEST_valueset: + summary: Technical test using a value set result + value: + SiteID: 1 + TestSiteCode: TEST_VSET + TestSiteName: Urine Color + TestType: TEST + SeqScr: 140 + SeqRpt: 140 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 43800 + RefTxt: NORM=Normal;MACRO=Macro + Flag: 'N' + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + Method: Visual + TEST_valueset_map: + summary: Value set reference with test map + value: + SiteID: 1 + TestSiteCode: TEST_VMAP + TestSiteName: Urine Color (Map) + TestType: TEST + SeqScr: 145 + SeqRpt: 145 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 43800 + RefTxt: NORM=Normal;ABN=Abnormal + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '8' + details: + - HostTestCode: UCOLOR + HostTestName: Urine Color + ConDefID: 12 + ClientTestCode: UCOLOR_C + ClientTestName: Urine Color Client + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + TEST_valueset_map_no_reftxt: + summary: Value set result with mapping but without explicit text reference entries + value: + SiteID: 1 + TestSiteCode: TEST_VSETM + TestSiteName: Urine Result Map + TestType: TEST + SeqScr: 150 + SeqRpt: 150 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '8' + details: + - HostTestCode: UGLUC + HostTestName: Urine Glucose + ConDefID: 12 + ClientTestCode: UGLUC_C + ClientTestName: Urine Glucose Client + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + CALC_basic: + summary: Calculated test with members (no references) + value: + SiteID: 1 + TestSiteCode: CALC_BASE + TestSiteName: Estimated GFR + TestType: CALC + SeqScr: 190 + SeqRpt: 190 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 0 + DisciplineID: 2 + DepartmentID: 2 + testdefcal: + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + testdefgrp: + members: + - TestSiteID: 21 + - TestSiteID: 22 + CALC_full: + summary: Calculated test with numeric reference ranges and map + value: + SiteID: 1 + TestSiteCode: CALC_FULL + TestSiteName: Estimated GFR (Map) + TestType: CALC + SeqScr: 195 + SeqRpt: 195 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 0 + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 10 + HighSign: LE + High: 20 + AgeStart: 6570 + AgeEnd: 43800 + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: EGFR + HostTestName: eGFR + ConDefID: 1 + ClientTestCode: EGFR_C + ClientTestName: eGFR Client + DisciplineID: 2 + DepartmentID: 2 + testdefcal: + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + testdefgrp: + members: + - TestSiteID: 21 + - TestSiteID: 22 + GROUP_with_members: + summary: Group/profile test with members and mapping + value: + SiteID: 1 + TestSiteCode: GROUP_PNL + TestSiteName: Lipid Profile + TestType: GROUP + SeqScr: 10 + SeqRpt: 10 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: LIPID + HostTestName: Lipid Profile + ConDefID: 1 + ClientTestCode: LIPID_C + ClientTestName: Lipid Client + testdefgrp: + members: + - TestSiteID: 169 + - TestSiteID: 170 + responses: + '201': + description: Test definition created + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: created + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer + '400': + description: Validation error (e.g., invalid member TestSiteID) + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' + /api/test/{id}: + get: + tags: + - Test + summary: Get test definition by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Site ID + responses: + '200': + description: Test definition details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/TestDefinition' + '404': + description: Test not found + patch: + tags: + - Test + summary: Update test definition + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Site ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestSiteID: + type: integer + description: Test Site ID (required) + TestSiteCode: + type: string + TestSiteName: + type: string + TestType: + type: string + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + Description: + type: string + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: + - NMRIC + - RANGE + - TEXT + - VSET + - NORES + RefType: + type: string + enum: + - RANGE + - THOLD + - VSET + - TEXT + - NOREF + VSet: + type: integer + ReqQty: + type: number + format: decimal + ReqQtyUnit: + type: string + Unit1: + type: string + Factor: + type: number + format: decimal + Unit2: + type: string + Decimal: + type: integer + CollReq: + type: string + Method: + type: string + ExpectedTAT: + type: integer + SeqScr: + type: integer + SeqRpt: + type: integer + IndentLeft: + type: integer + FontStyle: + type: string + isVisibleScr: + type: integer + isVisibleRpt: + type: integer + isCountStat: + type: integer + testdefcal: + type: object + description: Calculated test metadata persisted in the `testdefcal` table. + properties: + FormulaCode: + type: string + description: Formula expression for calculated tests (e.g., "{TBIL} - {DBIL}") + testdefgrp: + type: object + description: Group definition wrapper for CALC/GROUP member assignments. + properties: + members: + type: array + description: Array of member TestSiteIDs for CALC/GROUP definitions. + items: + type: object + properties: + TestSiteID: + type: integer + description: Foreign key referencing the member test's TestSiteID. + required: + - TestSiteID + refnum: + type: array + items: + type: object + reftxt: + type: array + items: + type: object + testmap: + type: array + items: + type: object + properties: + HostType: + type: string + HostID: + type: string + HostTestCode: + type: string + HostTestName: + type: string + ClientType: + type: string + ClientID: + type: string + ClientTestCode: + type: string + ClientTestName: + type: string + ConDefID: + type: integer + nullable: true + required: + - TestSiteID + responses: + '200': + description: Test definition updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer + '400': + description: Validation error (e.g., invalid member TestSiteID) + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' + delete: + tags: + - Test + summary: Soft delete test definition + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Site ID to delete + requestBody: + content: + application/json: + schema: + type: object + properties: + TestSiteID: + type: integer + description: Optional - can be provided in body instead of path + responses: + '200': + description: Test disabled successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer + EndDate: + type: string + format: date-time + '404': + description: Test not found + '422': + description: Test already disabled + /api/user: + get: + tags: + - User + summary: List users with pagination and search + security: + - bearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number + - name: per_page + in: query + schema: + type: integer + default: 20 + description: Items per page + - name: search + in: query + schema: + type: string + description: Search term for username, email, or name + responses: + '200': + description: List of users with pagination + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Users retrieved successfully + data: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + pagination: + type: object + properties: + current_page: + type: integer + per_page: + type: integer + total: + type: integer + total_pages: + type: integer + '500': + description: Server error + post: + tags: + - User + summary: Create new user + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreate' + responses: + '201': + description: User created successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User created successfully + data: + type: object + properties: + UserID: + type: integer + Username: + type: string + Email: + type: string + '400': + description: Validation failed + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: Validation failed + data: + type: object + '500': + description: Server error + /api/user/{id}: + get: + tags: + - User + summary: Get user by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User details + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User not found + '500': + description: Server error + patch: + tags: + - User + summary: Update existing user + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdate' + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User updated successfully + data: + type: object + properties: + UserID: + type: integer + updated_fields: + type: array + items: + type: string + '400': + description: UserID is required + '404': + description: User not found + '500': + description: Server error + delete: + tags: + - User + summary: Delete user (soft delete) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User deleted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User deleted successfully + data: + type: object + properties: + UserID: + type: integer + '404': + description: User not found + '500': + description: Server error + /api/valueset: + get: + tags: + - ValueSet + summary: List lib value sets + description: List all library/system value sets from JSON files with item counts. Returns an array of objects with value, label, and count properties. + security: + - bearerAuth: [] + parameters: + - name: search + in: query + schema: + type: string + description: Optional search term to filter value set names or labels + responses: + '200': + description: List of lib value sets with item counts + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + data: + type: array + items: + $ref: '#/components/schemas/ValueSetListItem' + example: + - value: sex + label: Sex + count: 3 + - value: marital_status + label: Marital Status + count: 6 + - value: order_status + label: Order Status + count: 6 + /api/valueset/{key}: + get: + tags: + - ValueSet + summary: Get lib value set by key + description: | + Get a specific library/system value set from JSON files. + + **Available value set keys:** + - `activity_result` - Activity Result + - `additive` - Additive + - `adt_event` - ADT Event + - `area_class` - Area Class + - `body_site` - Body Site + - `collection_method` - Collection Method + - `container_cap_color` - Container Cap Color + - `container_class` - Container Class + - `container_size` - Container Size + - `country` - Country + - `death_indicator` - Death Indicator + - `did_type` - DID Type + - `enable_disable` - Enable/Disable + - `entity_type` - Entity Type + - `ethnic` - Ethnic + - `fasting_status` - Fasting Status + - `formula_language` - Formula Language + - `generate_by` - Generate By + - `identifier_type` - Identifier Type + - `location_type` - Location Type + - `marital_status` - Marital Status + - `math_sign` - Math Sign + - `numeric_ref_type` - Numeric Reference Type + - `operation` - Operation (CRUD) + - `order_priority` - Order Priority + - `order_status` - Order Status + - `race` - Race (Ethnicity) + - `range_type` - Range Type + - `reference_type` - Reference Type + - `religion` - Religion + - `requested_entity` - Requested Entity + - `result_type` - Result Type + - `result_unit` - Result Unit + - `sex` - Sex + - `site_class` - Site Class + - `site_type` - Site Type + - `specimen_activity` - Specimen Activity + - `specimen_condition` - Specimen Condition + - `specimen_role` - Specimen Role + - `specimen_status` - Specimen Status + - `specimen_type` - Specimen Type + - `test_activity` - Test Activity + - `test_type` - Test Type + - `text_ref_type` - Text Reference Type + - `unit` - Unit + - `v_category` - VCategory + - `ws_type` - Workstation Type + security: + - bearerAuth: [] + parameters: + - name: key + in: path + required: true + schema: + type: string + enum: + - activity_result + - additive + - adt_event + - area_class + - body_site + - collection_method + - container_cap_color + - container_class + - container_size + - country + - death_indicator + - did_type + - enable_disable + - entity_type + - ethnic + - fasting_status + - formula_language + - generate_by + - identifier_type + - location_type + - marital_status + - math_sign + - numeric_ref_type + - operation + - order_priority + - order_status + - race + - range_type + - reference_type + - religion + - requested_entity + - result_type + - result_unit + - sex + - site_class + - site_type + - specimen_activity + - specimen_condition + - specimen_role + - specimen_status + - specimen_type + - test_activity + - test_type + - text_ref_type + - unit + - v_category + - ws_type + description: Value set key name + responses: + '200': + description: Lib value set details + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: array + items: + $ref: '#/components/schemas/ValueSetLibItem' + /api/valueset/refresh: + post: + tags: + - ValueSet + summary: Refresh lib ValueSet cache + description: Clear and reload the library/system ValueSet cache from JSON files. Call this after modifying JSON files in app/Libraries/Data/. + security: + - bearerAuth: [] + responses: + '200': + description: Lib ValueSet cache refreshed + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Cache cleared + /api/valueset/user/items: + get: + tags: + - ValueSet + summary: List user value set items + description: List value set items from database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: VSetID + in: query + schema: + type: integer + description: Filter by ValueSet ID + - name: search + in: query + schema: + type: string + description: Search term to filter by VValue, VDesc, or VSName + - name: param + in: query + schema: + type: string + description: Alternative search parameter (alias for search) + responses: + '200': + description: List of user value set items + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: array + items: + $ref: '#/components/schemas/ValueSetItem' + post: + tags: + - ValueSet + summary: Create user value set item + description: Create value set item in database (user-defined) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - VSetID + properties: + SiteID: + type: integer + description: Site reference (default 1) + VSetID: + type: integer + description: Reference to value set definition (required) + VOrder: + type: integer + description: Display order (default 0) + VValue: + type: string + description: The value code + VDesc: + type: string + description: The display description/label + responses: + '201': + description: User value set item created + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/ValueSetItem' + /api/valueset/user/items/{id}: + get: + tags: + - ValueSet + summary: Get user value set item by ID + description: Get value set item from database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User value set item details + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + $ref: '#/components/schemas/ValueSetItem' + put: + tags: + - ValueSet + summary: Update user value set item + description: Update value set item in database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + SiteID: + type: integer + description: Site reference + VSetID: + type: integer + description: Reference to value set definition + VOrder: + type: integer + description: Display order + VValue: + type: string + description: The value code + VDesc: + type: string + description: The display description/label + responses: + '200': + description: User value set item updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/ValueSetItem' + delete: + tags: + - ValueSet + summary: Delete user value set item + description: Delete value set item from database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User value set item deleted + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + /api/valueset/user/def: + get: + tags: + - ValueSet + summary: List user value set definitions + description: List value set definitions from database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: search + in: query + schema: + type: string + description: Optional search term to filter definitions + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number for pagination + - name: limit + in: query + schema: + type: integer + default: 100 + description: Number of items per page + responses: + '200': + description: List of user value set definitions + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: array + items: + $ref: '#/components/schemas/ValueSetDef' + meta: + type: object + properties: + total: + type: integer + page: + type: integer + limit: + type: integer + post: + tags: + - ValueSet + summary: Create user value set definition + description: Create value set definition in database (user-defined) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + SiteID: + type: integer + description: Site reference (default 1) + VSName: + type: string + description: Value set name + VSDesc: + type: string + description: Value set description + responses: + '201': + description: User value set definition created + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/ValueSetDef' + /api/valueset/user/def/{id}: + get: + tags: + - ValueSet + summary: Get user value set definition by ID + description: Get value set definition from database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User value set definition details + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + $ref: '#/components/schemas/ValueSetDef' + put: + tags: + - ValueSet + summary: Update user value set definition + description: Update value set definition in database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + SiteID: + type: integer + description: Site reference + VSName: + type: string + description: Value set name + VSDesc: + type: string + description: Value set description + responses: + '200': + description: User value set definition updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/ValueSetDef' + delete: + tags: + - ValueSet + summary: Delete user value set definition + description: Delete value set definition from database (user-defined) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User value set definition deleted + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT token from login endpoint + cookieAuth: + type: apiKey + in: cookie + name: token + description: JWT token stored in HTTP-only cookie + schemas: + SuccessResponse: + type: object + properties: + status: + type: string + example: success + message: + type: string + code: + type: integer + example: 200 + ErrorResponse: + type: object + properties: + status: + type: string + example: error + message: + type: string + errors: + type: object + DashboardSummary: + type: object + properties: + pendingOrders: + type: integer + todayResults: + type: integer + criticalResults: + type: integer + activePatients: + type: integer + LoginRequest: + type: object + required: + - username + - password + properties: + username: + type: string + example: labuser01 + password: + type: string + format: password + example: secret123 + LoginResponse: + type: object + properties: + status: + type: string + example: success + code: + type: integer + example: 200 + message: + type: string + example: Login successful + RegisterRequest: + type: object + required: + - username + - password + - email + properties: + username: + type: string + password: + type: string + format: password + email: + type: string + format: email + full_name: + type: string + Patient: + type: object + required: + - PatientID + - Sex + - NameFirst + - Birthdate + properties: + PatientID: + type: string + maxLength: 30 + pattern: ^[A-Za-z0-9]+$ + description: Internal patient identifier + AlternatePID: + type: string + maxLength: 30 + pattern: ^[A-Za-z0-9]+$ + Prefix: + type: string + maxLength: 10 + enum: + - Mr + - Mrs + - Ms + - Dr + - Prof + Sex: + type: string + enum: + - '1' + - '2' + description: '1: Female, 2: Male' + NameFirst: + type: string + minLength: 1 + maxLength: 60 + pattern: ^[A-Za-z'\. ]+$ + NameMiddle: + type: string + minLength: 1 + maxLength: 60 + NameMaiden: + type: string + minLength: 1 + maxLength: 60 + NameLast: + type: string + minLength: 1 + maxLength: 60 + Suffix: + type: string + maxLength: 10 + Birthdate: + type: string + format: date-time + description: ISO 8601 UTC datetime + PlaceOfBirth: + type: string + maxLength: 100 + Citizenship: + type: string + maxLength: 100 + Street_1: + type: string + maxLength: 255 + Street_2: + type: string + maxLength: 255 + Street_3: + type: string + maxLength: 255 + ZIP: + type: string + maxLength: 10 + pattern: ^[0-9]+$ + Phone: + type: string + pattern: ^\+?[0-9]{8,15}$ + MobilePhone: + type: string + pattern: ^\+?[0-9]{8,15}$ + EmailAddress1: + type: string + format: email + maxLength: 100 + EmailAddress2: + type: string + format: email + maxLength: 100 + PatIdt: + $ref: '#/components/schemas/PatientIdentifier' + LinkTo: + type: array + description: Array of linked patient references + items: + $ref: '#/components/schemas/LinkedPatient' + Custodian: + $ref: '#/components/schemas/Custodian' + isDead: + type: string + enum: + - '0' + - '1' + description: '0: No (alive), 1: Yes (deceased)' + TimeOfDeath: + type: string + format: date-time + description: ISO 8601 UTC datetime of death + PatCom: + type: string + description: Patient comment/notes + PatAtt: + type: array + description: Patient address entries + items: + $ref: '#/components/schemas/PatAttEntry' + Province: + type: integer + description: Province AreaGeoID (foreign key to areageo table) + ProvinceLabel: + type: string + description: Province name (resolved from areageo) + City: + type: integer + description: City AreaGeoID (foreign key to areageo table) + CityLabel: + type: string + description: City name (resolved from areageo) + Country: + type: string + maxLength: 10 + description: Country ISO 3-letter code (e.g., IDN, USA) + CountryLabel: + type: string + description: Country name (resolved from valueset) + Race: + type: string + maxLength: 100 + MaritalStatus: + type: string + enum: + - A + - B + - D + - M + - S + - W + description: 'A: Annulled, B: Separated, D: Divorced, M: Married, S: Single, W: Widowed' + Religion: + type: string + maxLength: 100 + Ethnic: + type: string + maxLength: 100 + PatientIdentifier: + type: object + properties: + IdentifierType: + type: string + enum: + - KTP + - PASS + - SSN + - SIM + - KTAS + description: | + KTP: 16 digit numeric + PASS: alphanumeric max 9 + SSN: 9 digit numeric + SIM: 19-20 digit numeric + KTAS: 11 digit numeric + Identifier: + type: string + maxLength: 255 + LinkedPatient: + type: object + description: Linked patient reference + properties: + InternalPID: + type: integer + description: Internal patient ID of the linked patient + PatientID: + type: string + description: Patient ID of the linked patient + Custodian: + type: object + description: Patient custodian/guardian + properties: + InternalPID: + type: integer + description: Internal patient ID of the custodian + PatientID: + type: string + description: Patient ID of the custodian + PatAttEntry: + type: object + description: Patient address/attorney entry + properties: + Address: + type: string + description: Address text + PatientListResponse: + type: object + properties: + status: + type: string + example: success + data: + type: array + items: + $ref: '#/components/schemas/Patient' + pagination: + type: object + properties: + page: + type: integer + perPage: + type: integer + total: + type: integer + PatientVisit: + type: object + properties: + InternalPVID: + type: integer + description: Primary key (auto-generated) + PVID: + type: string + description: Visit ID (auto-generated with DV prefix if not provided) + InternalPID: + type: integer + description: Reference to patient + EpisodeID: + type: string + description: Episode identifier + SiteID: + type: integer + description: Site reference + LastLocation: + type: string + description: Full name of the last/current location from patvisitadt + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + ArchivedDate: + type: string + format: date-time + DelDate: + type: string + format: date-time + PatDiag: + type: object + description: Diagnosis information (optional) + properties: + DiagCode: + type: string + Diagnosis: + type: string + PatVisitADT: + type: object + description: ADT (Admission/Discharge/Transfer) information (optional) + properties: + ADTCode: + type: string + enum: + - A01 + - A02 + - A03 + - A04 + - A08 + LocationID: + type: integer + AttDoc: + type: integer + description: Attending physician ContactID + RefDoc: + type: integer + description: Referring physician ContactID + AdmDoc: + type: integer + description: Admitting physician ContactID + CnsDoc: + type: integer + description: Consulting physician ContactID + PatVisitADT: + type: object + properties: + PVADTID: + type: integer + description: Primary key (auto-generated) + InternalPVID: + type: integer + description: Reference to patient visit + ADTCode: + type: string + enum: + - A01 + - A02 + - A03 + - A04 + - A08 + description: | + A01: Admit + A02: Transfer + A03: Discharge + A04: Register + A08: Update + LocationID: + type: integer + description: Location/ward reference + AttDoc: + type: integer + description: Attending physician ContactID + RefDoc: + type: integer + description: Referring physician ContactID + AdmDoc: + type: integer + description: Admitting physician ContactID + CnsDoc: + type: integer + description: Consulting physician ContactID + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + ArchivedDate: + type: string + format: date-time + DelDate: + type: string + format: date-time + Account: + type: object + properties: + id: + type: integer + AccountName: + type: string + AccountCode: + type: string + AccountType: + type: string + Site: + type: object + properties: + id: + type: integer + SiteName: + type: string + SiteCode: + type: string + maxLength: 2 + pattern: ^[A-Z0-9]{2}$ + AccountID: + type: integer + Discipline: + type: object + properties: + id: + type: integer + DisciplineName: + type: string + DisciplineCode: + type: string + SeqScr: + type: integer + description: Display order on screen + SeqRpt: + type: integer + description: Display order in reports + Department: + type: object + properties: + id: + type: integer + DeptName: + type: string + DeptCode: + type: string + SiteID: + type: integer + Workstation: + type: object + properties: + id: + type: integer + WorkstationName: + type: string + WorkstationCode: + type: string + SiteID: + type: integer + DepartmentID: + type: integer + HostApp: + type: object + properties: + HostAppID: + type: string + maxLength: 5 + HostAppName: + type: string + SiteID: + type: integer + SiteName: + type: string + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + HostComPara: + type: object + properties: + HostAppID: + type: string + maxLength: 5 + HostAppName: + type: string + HostIP: + type: string + maxLength: 15 + HostPort: + type: string + maxLength: 6 + HostPwd: + type: string + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + CodingSys: + type: object + properties: + CodingSysID: + type: integer + CodingSysAbb: + type: string + maxLength: 6 + FullText: + type: string + Description: + type: string + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + Specimen: + type: object + properties: + id: + type: integer + SpecimenID: + type: string + PatientID: + type: string + SpecimenType: + type: string + description: Specimen type code + SpecimenTypeLabel: + type: string + description: Specimen type display text + CollectionDate: + type: string + format: date-time + CollectionMethod: + type: string + description: Collection method code + CollectionMethodLabel: + type: string + description: Collection method display text + ContainerID: + type: integer + SpecimenStatus: + type: string + description: Specimen status code + SpecimenStatusLabel: + type: string + description: Specimen status display text + BodySite: + type: string + description: Body site code + BodySiteLabel: + type: string + description: Body site display text + ContainerDef: + type: object + properties: + id: + type: integer + ContainerCode: + type: string + ContainerName: + type: string + ConCategory: + type: string + description: Container category code + ConCategoryLabel: + type: string + description: Container category display text + ConSize: + type: string + description: Container size code + ConSizeLabel: + type: string + description: Container size display text + CapColor: + type: string + description: Cap color code + CapColorLabel: + type: string + description: Cap color display text + SpecimenPrep: + type: object + properties: + id: + type: integer + PrepCode: + type: string + PrepName: + type: string + Description: + type: string + SpecimenStatus: + type: object + properties: + id: + type: integer + StatusCode: + type: string + StatusName: + type: string + Description: + type: string + Status: + type: string + description: Status code + StatusLabel: + type: string + description: Status display text + Activity: + type: string + description: Activity code + ActivityLabel: + type: string + description: Activity display text + SpecimenCollection: + type: object + properties: + id: + type: integer + CollectionCode: + type: string + CollectionName: + type: string + Description: + type: string + CollectionMethod: + type: string + description: Collection method code + CollectionMethodLabel: + type: string + description: Collection method display text + Additive: + type: string + description: Additive code + AdditiveLabel: + type: string + description: Additive display text + SpecimenRole: + type: string + description: Specimen role code + SpecimenRoleLabel: + type: string + description: Specimen role display text + TestDefinition: + type: object + properties: + TestSiteID: + type: integer + SiteID: + type: integer + TestSiteCode: + type: string + TestSiteName: + type: string + TestType: + type: string + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + description: | + TEST: Technical test + PARAM: Parameter + CALC: Calculated + GROUP: Panel/Profile + TITLE: Section header + Description: + type: string + DisciplineID: + type: integer + DisciplineName: + type: string + DepartmentID: + type: integer + DepartmentName: + type: string + ResultType: + type: string + enum: + - NMRIC + - RANGE + - TEXT + - VSET + - NORES + description: | + Result type determines the format of test results: + - NMRIC: Single numeric value + - RANGE: Numeric range (min-max) + - TEXT: Free text result + - VSET: Value set/enum result + - NORES: No result (for GROUP and TITLE types) + + TestType to ResultType mapping: + - TEST: NMRIC | RANGE | TEXT | VSET + - PARAM: NMRIC | RANGE | TEXT | VSET + - CALC: NMRIC (calculated result is always numeric) + - GROUP: NORES (no result, container only) + - TITLE: NORES (no result, header only) + RefType: + type: string + enum: + - RANGE + - THOLD + - VSET + - TEXT + - NOREF + description: | + Reference type determines which reference range table to use: + - RANGE: Numeric reference range + - THOLD: Threshold/panic range + - VSET: Value set reference + - TEXT: Free text reference + - NOREF: No reference (for NORES result type) + + ResultType to RefType mapping: + - NMRIC: RANGE | THOLD → refnum table + - RANGE: RANGE | THOLD → refnum table + - VSET: VSET → reftxt table + - TEXT: TEXT → reftxt table + - NORES: NOREF → (no reference table) + VSet: + type: integer + description: Value set ID for VSET result type + ReqQty: + type: number + format: decimal + description: Required sample quantity + ReqQtyUnit: + type: string + description: Unit for required quantity + Unit1: + type: string + description: Primary unit + Factor: + type: number + format: decimal + description: Conversion factor + Unit2: + type: string + description: Secondary unit (after conversion) + Decimal: + type: integer + description: Number of decimal places + CollReq: + type: string + description: Collection requirements + Method: + type: string + description: Test method + ExpectedTAT: + type: integer + description: Expected turnaround time + SeqScr: + type: integer + description: Screen sequence + SeqRpt: + type: integer + description: Report sequence + IndentLeft: + type: integer + default: 0 + FontStyle: + type: string + isVisibleScr: + type: integer + default: 1 + description: Screen visibility (0=hidden, 1=visible) + isVisibleRpt: + type: integer + default: 1 + description: Report visibility (0=hidden, 1=visible) + isCountStat: + type: integer + default: 1 + Level: + type: integer + isRequestable: + type: integer + default: 1 + description: Flag indicating if test can be requested (1=yes, 0=no) + CreateDate: + type: string + format: date-time + StartDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + FormulaCode: + type: string + description: Formula expression for calculated tests + testdefcal: + type: array + description: Calculated test details (only for CALC type) + items: + type: object + testdefgrp: + type: object + description: Group definition payload for GROUP and CALC types. + properties: + members: + type: array + description: | + Group members (for GROUP and CALC types). + When creating or updating, provide members in testdefgrp.members with TestSiteID field. + Do NOT use Member, SeqScr, or Members fields when creating/updating. + items: + type: object + properties: + TestGrpID: + type: integer + description: Group membership record ID + TestSiteID: + type: integer + description: Parent group TestSiteID + Member: + type: integer + description: | + Member TestSiteID (foreign key to testdefsite). + **Note**: This field is in the response. When creating/updating, use TestSiteID in testdefgrp.members instead. + TestSiteCode: + type: string + description: Member test code + TestSiteName: + type: string + description: Member test name + TestType: + type: string + description: Member test type + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + testmap: + type: array + description: Flat test mapping payload for /api/test create/update + items: + type: object + properties: + HostType: + type: string + HostID: + type: string + HostTestCode: + type: string + HostTestName: + type: string + ClientType: + type: string + ClientID: + type: string + ClientTestCode: + type: string + ClientTestName: + type: string + ConDefID: + type: integer + nullable: true + refnum: + type: array + description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type. + items: + type: object + properties: + RefNumID: + type: integer + NumRefType: + type: string + enum: + - NMRC + - THOLD + description: NMRC=Numeric range, THOLD=Threshold + NumRefTypeLabel: + type: string + RangeType: + type: string + RangeTypeLabel: + type: string + SpcType: + type: string + description: Specimen type code (e.g., GEN, EDTA) + Sex: + type: string + SexLabel: + type: string + LowSign: + type: string + LowSignLabel: + type: string + HighSign: + type: string + HighSignLabel: + type: string + High: + type: number + format: float + Low: + type: number + format: float + AgeStart: + type: integer + description: Minimum patient age in days + AgeEnd: + type: integer + description: Maximum patient age in days + Flag: + type: string + Interpretation: + type: string + Notes: + type: string + description: Optional note attached to the numeric reference range + reftxt: + type: array + description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type. + items: + type: object + properties: + RefTxtID: + type: integer + TxtRefType: + type: string + enum: + - TEXT + - VSET + description: TEXT=Free text, VSET=Value set + TxtRefTypeLabel: + type: string + Sex: + type: string + SexLabel: + type: string + AgeStart: + type: integer + description: Minimum patient age in days + AgeEnd: + type: integer + description: Maximum patient age in days + RefTxt: + type: string + Flag: + type: string + examples: + TEST_numeric: + summary: Technical test with numeric reference + value: + TestSiteID: 1 + SiteID: 1 + TestSiteCode: GLU + TestSiteName: Glucose + TestType: TEST + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: NMRC + Unit1: mg/dL + ReqQty: 300 + ReqQtyUnit: uL + Decimal: 0 + Method: Hexokinase + SeqScr: 11 + SeqRpt: 11 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + refnum: + - RefNumID: 1 + NumRefType: NMRC + NumRefTypeLabel: Numeric + RangeType: REF + RangeTypeLabel: Reference Range + Sex: '2' + SexLabel: Male + LowSign: GE + LowSignLabel: '>=' + HighSign: LE + HighSignLabel: <= + Low: 70 + High: 100 + AgeStart: 6570 + AgeEnd: 36135 + Flag: 'N' + Interpretation: Normal + TEST_threshold: + summary: Technical test with threshold reference (panic) + value: + TestSiteID: 2 + SiteID: 1 + TestSiteCode: GLU + TestSiteName: Glucose + TestType: TEST + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mg/dL + Decimal: 0 + Method: Hexokinase + SeqScr: 11 + SeqRpt: 11 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + refnum: + - RefNumID: 2 + NumRefType: THOLD + NumRefTypeLabel: Threshold + RangeType: PANIC + RangeTypeLabel: Panic Range + Sex: '1' + SexLabel: Female + LowSign: LT + LowSignLabel: < + High: 40 + AgeStart: 0 + AgeEnd: 43800 + Flag: L + Interpretation: Critical Low + TEST_text: + summary: Technical test with text reference + value: + TestSiteID: 3 + SiteID: 1 + TestSiteCode: STAGE + TestSiteName: Disease Stage + TestType: TEST + DisciplineID: 1 + DepartmentID: 1 + ResultType: VSET + RefType: TEXT + SeqScr: 50 + SeqRpt: 50 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + reftxt: + - RefTxtID: 1 + TxtRefType: TEXT + TxtRefTypeLabel: Text + Sex: '2' + SexLabel: Male + AgeStart: 6570 + AgeEnd: 36135 + RefTxt: NORM=Normal;HYPO=Hypochromic;MACRO=Macrocytic + Flag: 'N' + PARAM: + summary: Parameter test + value: + TestSiteID: 4 + SiteID: 1 + TestSiteCode: HEIGHT + TestSiteName: Height + TestType: PARAM + DisciplineID: 10 + ResultType: NMRIC + Unit1: cm + Decimal: 0 + SeqScr: 40 + SeqRpt: 40 + isVisibleScr: 1 + isVisibleRpt: 0 + isCountStat: 0 + CALC: + summary: Calculated test with reference + value: + TestSiteID: 5 + SiteID: 1 + TestSiteCode: EGFR + TestSiteName: eGFR + TestType: CALC + DisciplineID: 2 + DepartmentID: 2 + Unit1: mL/min/1.73m2 + Decimal: 0 + SeqScr: 20 + SeqRpt: 20 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 0 + testdefcal: + - TestCalID: 1 + DisciplineID: 2 + DepartmentID: 2 + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + Unit1: mL/min/1.73m2 + Decimal: 0 + testdefgrp: + members: + - TestSiteID: 21 + TestSiteCode: CREA + TestSiteName: Creatinine + TestType: TEST + - TestSiteID: 51 + TestSiteCode: AGE + TestSiteName: Age + TestType: PARAM + refnum: + - RefNumID: 5 + NumRefType: NMRC + NumRefTypeLabel: Numeric + RangeType: REF + RangeTypeLabel: Reference Range + Sex: '1' + SexLabel: Female + LowSign: GE + LowSignLabel: '>=' + HighSign: LE + HighSignLabel: <= + Low: 10 + High: 20 + AgeStart: 6570 + AgeEnd: 43800 + Flag: 'N' + Interpretation: Normal + GROUP: + summary: Panel/Profile test + value: + TestSiteID: 6 + SiteID: 1 + TestSiteCode: LIPID + TestSiteName: Lipid Panel + TestType: GROUP + DisciplineID: 2 + DepartmentID: 2 + SeqScr: 51 + SeqRpt: 51 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 1 + testdefgrp: + members: + - TestGrpID: 1 + TestSiteID: 6 + Member: 100 + MemberTestSiteID: 100 + TestSiteCode: CHOL + TestSiteName: Total Cholesterol + TestType: TEST + - TestGrpID: 2 + TestSiteID: 6 + Member: 101 + MemberTestSiteID: 101 + TestSiteCode: TG + TestSiteName: Triglycerides + TestType: TEST + TITLE: + summary: Section header + value: + TestSiteID: 7 + SiteID: 1 + TestSiteCode: CHEM_HEADER + TestSiteName: '--- CHEMISTRY ---' + TestType: TITLE + DisciplineID: 2 + DepartmentID: 2 + SeqScr: 100 + SeqRpt: 100 + isVisibleScr: 1 + isVisibleRpt: 1 + isCountStat: 0 + TestMap: + type: object + properties: + TestMapID: + type: integer + HostType: + type: string + description: Host type code (e.g., SITE, WORKSTATION, INSTRUMENT) + HostID: + type: string + description: Host identifier + ClientType: + type: string + description: Client type code (e.g., SITE, WORKSTATION, INSTRUMENT) + ClientID: + type: string + description: Client identifier + HostName: + type: string + description: Resolved host name (from view) + ClientName: + type: string + description: Resolved client name (from view) + details: + type: array + description: Test mapping detail records + items: + $ref: '#/components/schemas/TestMapDetail' + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + description: Soft delete timestamp + OrderTestList: + type: object + properties: + InternalOID: + type: integer + description: Internal order ID + OrderID: + type: string + description: Order ID (e.g., 0025030300001) + PlacerID: + type: string + nullable: true + InternalPID: + type: integer + description: Patient internal ID + SiteID: + type: integer + PVADTID: + type: integer + description: Visit ADT ID + ReqApp: + type: string + nullable: true + Priority: + type: string + enum: + - R + - S + - U + description: | + R: Routine + S: Stat + U: Urgent + PriorityLabel: + type: string + description: Priority display text + TrnDate: + type: string + format: date-time + description: Transaction/Order date + EffDate: + type: string + format: date-time + description: Effective date + CreateDate: + type: string + format: date-time + OrderStatus: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + description: | + ORD: Ordered + SCH: Scheduled + ANA: Analysis + VER: Verified + REV: Reviewed + REP: Reported + OrderStatusLabel: + type: string + description: Order status display text + OrderTest: + type: object + properties: + InternalOID: + type: integer + description: Internal order ID + OrderID: + type: string + description: Order ID (e.g., 0025030300001) + PlacerID: + type: string + nullable: true + InternalPID: + type: integer + description: Patient internal ID + SiteID: + type: integer + PVADTID: + type: integer + description: Visit ADT ID + ReqApp: + type: string + nullable: true + Priority: + type: string + enum: + - R + - S + - U + description: | + R: Routine + S: Stat + U: Urgent + PriorityLabel: + type: string + description: Priority display text + TrnDate: + type: string + format: date-time + description: Transaction/Order date + EffDate: + type: string + format: date-time + description: Effective date + CreateDate: + type: string + format: date-time + OrderStatus: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + description: | + ORD: Ordered + SCH: Scheduled + ANA: Analysis + VER: Verified + REV: Reviewed + REP: Reported + OrderStatusLabel: + type: string + description: Order status display text + Specimens: + type: array + items: + $ref: '#/components/schemas/OrderSpecimen' + description: Associated specimens for this order + Tests: + type: array + items: + $ref: '#/components/schemas/OrderTestItem' + description: Test results (patres) for this order + OrderItem: + type: object + properties: + id: + type: integer + OrderID: + type: string + TestID: + type: integer + SpecimenID: + type: string + Status: + type: string + EdgeResultRequest: + type: object + required: + - sample_id + - instrument_id + properties: + sample_id: + type: string + description: Sample barcode/identifier + instrument_id: + type: string + description: Instrument identifier + patient_id: + type: string + description: Patient identifier (optional) + results: + type: array + items: + type: object + properties: + test_code: + type: string + result_value: + type: string + unit: + type: string + flags: + type: string + enum: + - H + - L + - 'N' + - A + description: H=High, L=Low, N=Normal, A=Abnormal + EdgeResultResponse: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Result received and queued + data: + type: object + properties: + edge_res_id: + type: integer + sample_id: + type: string + instrument_id: + type: string + EdgeOrder: + type: object + properties: + OrderID: + type: string + PatientID: + type: string + SampleID: + type: string + Tests: + type: array + items: + type: object + properties: + TestCode: + type: string + TestName: + type: string + SpecimenType: + type: string + Priority: + type: string + DueDateTime: + type: string + format: date-time + ValueSetLibItem: + type: object + description: Library/system value set item from JSON files + properties: + value: + type: string + description: The value/key code + label: + type: string + description: The display label + ValueSetDef: + type: object + description: User-defined value set definition (from database) + properties: + VSetID: + type: integer + description: Primary key + SiteID: + type: integer + description: Site reference + VSName: + type: string + description: Value set name + VSDesc: + type: string + description: Value set description + CreateDate: + type: string + format: date-time + description: Creation timestamp + EndDate: + type: string + format: date-time + nullable: true + description: Soft delete timestamp + ItemCount: + type: integer + description: Number of items in this value set + ValueSetItem: + type: object + description: User-defined value set item (from database) + properties: + VID: + type: integer + description: Primary key + SiteID: + type: integer + description: Site reference + VSetID: + type: integer + description: Reference to value set definition + VOrder: + type: integer + description: Display order + VValue: + type: string + description: The value code + VDesc: + type: string + description: The display description/label + VCategory: + type: string + description: Category code + CreateDate: + type: string + format: date-time + description: Creation timestamp + EndDate: + type: string + format: date-time + nullable: true + description: Soft delete timestamp + VSName: + type: string + description: Value set name (from joined definition) + Location: + type: object + properties: + LocationID: + type: integer + description: Primary key + SiteID: + type: integer + description: Reference to site + LocCode: + type: string + maxLength: 6 + description: Location code (short identifier) + Parent: + type: integer + nullable: true + description: Parent location ID for hierarchical locations + LocFull: + type: string + maxLength: 255 + description: Full location name + Description: + type: string + maxLength: 255 + description: Location description + LocType: + type: string + description: Location type code (e.g., ROOM, WARD, BUILDING) + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + nullable: true + EquipmentList: + type: object + properties: + EID: + type: integer + description: Equipment ID (auto-increment) + IEID: + type: string + maxLength: 50 + description: Internal Equipment ID + DepartmentID: + type: integer + description: Reference to department + InstrumentID: + type: string + maxLength: 150 + description: Instrument identifier + InstrumentName: + type: string + maxLength: 150 + description: Instrument display name + WorkstationID: + type: integer + description: Reference to workstation + Enable: + type: integer + enum: + - 0 + - 1 + description: Equipment status (0=disabled, 1=enabled) + EquipmentRole: + type: string + maxLength: 1 + description: Equipment role code + CreateDate: + type: string + format: date-time + description: Creation timestamp + EndDate: + type: string + format: date-time + nullable: true + description: Deletion timestamp (soft delete) + DepartmentName: + type: string + description: Joined department name + WorkstationName: + type: string + description: Joined workstation name + User: + type: object + properties: + UserID: + type: integer + description: Unique user identifier + Username: + type: string + description: Unique login username + Email: + type: string + format: email + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role (admin, technician, doctor, etc.) + Department: + type: string + description: Department name + IsActive: + type: boolean + description: Whether the user account is active + CreatedAt: + type: string + format: date-time + description: Creation timestamp + UpdatedAt: + type: string + format: date-time + description: Last update timestamp + DelDate: + type: string + format: date-time + nullable: true + description: Soft delete timestamp (null if active) + UserCreate: + type: object + required: + - Username + - Email + properties: + Username: + type: string + minLength: 3 + maxLength: 50 + description: Unique login username + Email: + type: string + format: email + maxLength: 100 + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role + Department: + type: string + description: Department name + IsActive: + type: boolean + default: true + description: Whether the user account is active + UserUpdate: + type: object + required: + - UserID + properties: + UserID: + type: integer + description: User ID to update + Email: + type: string + format: email + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role + Department: + type: string + description: Department name + IsActive: + type: boolean + description: Whether the user account is active + UserListResponse: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Users retrieved successfully + data: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + pagination: + type: object + properties: + current_page: + type: integer + per_page: + type: integer + total: + type: integer + total_pages: + type: integer + RuleDef: + type: object + properties: + RuleID: + type: integer + RuleCode: + type: string + example: AUTO_SET_RESULT + RuleName: + type: string + example: Automatically Set Result + Description: + type: string + nullable: true + EventCode: + type: string + example: ORDER_CREATED + ConditionExpr: + type: string + nullable: true + description: Raw DSL expression (editable) + example: if(sex('M'); result_set(0.5); result_set(0.6)) + ConditionExprCompiled: + type: string + nullable: true + description: Compiled JSON structure (auto-generated from ConditionExpr) + example: '{"conditionExpr":"patient[\"Sex\"] == \"M\"","valueExpr":"(patient[\"Sex\"] == \"M\") ? 0.5 : 0.6","then":[{"type":"RESULT_SET","value":0.5,"valueExpr":"0.5"}],"else":[{"type":"RESULT_SET","value":0.6,"valueExpr":"0.6"}]}' + CreateDate: + type: string + format: date-time + nullable: true + StartDate: + type: string + format: date-time + nullable: true + EndDate: + type: string + format: date-time + nullable: true + RuleWithDetails: + allOf: + - $ref: '#/components/schemas/RuleDef' + - type: object + properties: + linkedTests: + type: array + items: + type: integer + description: Array of TestSiteIDs this rule is linked to. Rules are active only when attached to tests. + TestRule: + type: object + description: Mapping between a rule and a test site (testrule table). Rules are active when linked via this table. + properties: + TestRuleID: + type: integer + RuleID: + type: integer + TestSiteID: + type: integer + CreateDate: + type: string + format: date-time + nullable: true + EndDate: + type: string + format: date-time + nullable: true + AuditLogEntry: + type: object + properties: + LogPatientID: + type: integer + nullable: true + LogOrderID: + type: integer + nullable: true + LogMasterID: + type: integer + nullable: true + LogSystemID: + type: integer + nullable: true + TblName: + type: string + RecID: + type: string + FldName: + type: string + nullable: true + FldValuePrev: + type: string + nullable: true + FldValueNew: + type: string + nullable: true + UserID: + type: string + SiteID: + type: string + DIDType: + type: string + nullable: true + DID: + type: string + nullable: true + MachineID: + type: string + nullable: true + SessionID: + type: string + AppID: + type: string + ProcessID: + type: string + nullable: true + WebPageID: + type: string + nullable: true + EventID: + type: string + ActivityID: + type: string + Reason: + type: string + nullable: true + LogDate: + type: string + format: date-time + Context: + type: string + IpAddress: + type: string + nullable: true + AuditLogListResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AuditLogEntry' + pagination: + type: object + properties: + page: + type: integer + perPage: + type: integer + total: + type: integer + required: + - page + - perPage + - total + required: + - data + - pagination + AuditLogsEnvelope: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/AuditLogListResponse' + required: + - status + - message + - data + AuditLogsErrorResponse: + type: object + properties: + status: + type: string + message: + type: string + data: + nullable: true + required: + - status + - message + - data + Contact: + type: object + properties: + ContactID: + type: integer + description: Primary key + NameFirst: + type: string + description: First name (required) + NameLast: + type: string + description: Last name + Title: + type: string + description: Title (e.g., Dr, Mr, Mrs) + Initial: + type: string + description: Middle initial + Birthdate: + type: string + format: date-time + description: Date of birth + EmailAddress1: + type: string + format: email + description: Primary email address + EmailAddress2: + type: string + format: email + description: Secondary email address + Phone: + type: string + description: Primary phone number + MobilePhone1: + type: string + description: Primary mobile number + MobilePhone2: + type: string + description: Secondary mobile number + Specialty: + type: string + description: Medical specialty code + SubSpecialty: + type: string + description: Sub-specialty code + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + description: Occupation display text + ContactDetail: + type: object + properties: + ContactDetID: + type: integer + description: Primary key + ContactID: + type: integer + description: Parent contact ID + SiteID: + type: integer + nullable: true + description: Site identifier + ContactCode: + type: string + nullable: true + description: Contact code at site + ContactEmail: + type: string + nullable: true + description: Contact email address + OccupationID: + type: integer + nullable: true + description: Occupation reference + JobTitle: + type: string + nullable: true + description: Job title + Department: + type: string + nullable: true + description: Department name + ContactStartDate: + type: string + format: date-time + ContactEndDate: + type: string + format: date-time + nullable: true + ContactDetailOperations: + type: object + properties: + created: + type: array + description: New contact details to create + items: + $ref: '#/components/schemas/ContactDetail' + edited: + type: array + description: Existing contact details to update + items: + allOf: + - $ref: '#/components/schemas/ContactDetail' + - type: object + required: + - ContactDetID + deleted: + type: array + description: Contact detail IDs to soft delete + items: + type: integer + OrderSpecimen: + type: object + properties: + InternalSID: + type: integer + description: Internal specimen ID + SID: + type: string + description: Specimen ID (e.g., 0025030300001-S01) + SiteID: + type: integer + OrderID: + type: integer + description: Reference to internal order ID + ConDefID: + type: integer + description: Container Definition ID + nullable: true + ConCode: + type: string + description: Container code + nullable: true + ConName: + type: string + description: Container name + nullable: true + Qty: + type: integer + description: Quantity + Unit: + type: string + description: Unit of measurement + Status: + type: string + enum: + - PENDING + - COLLECTED + - RECEIVED + - PREPARED + - REJECTED + description: Current specimen status + GenerateBy: + type: string + description: Source that generated this specimen + CreateDate: + type: string + format: date-time + OrderTestItem: + type: object + properties: + ResultID: + type: integer + description: Unique result ID + OrderID: + type: integer + description: Reference to internal order ID + InternalSID: + type: integer + description: Reference to specimen + nullable: true + TestSiteID: + type: integer + description: Test definition site ID + TestSiteCode: + type: string + description: Test code + TestSiteName: + type: string + description: Test name + nullable: true + TestType: + type: string + description: Test type code identifying the test category + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + SID: + type: string + description: Order ID reference + SampleID: + type: string + description: Sample ID (same as OrderID) + SeqScr: + type: integer + nullable: true + description: Sequence number for this test on the screen + SeqRpt: + type: integer + nullable: true + description: Sequence number for this test in reports + Result: + type: string + description: Test result value + nullable: true + Discipline: + type: object + description: Discipline metadata used for ordering tests + properties: + DisciplineID: + type: integer + nullable: true + DisciplineCode: + type: string + nullable: true + DisciplineName: + type: string + nullable: true + SeqScr: + type: integer + nullable: true + description: Discipline sequence on the screen + SeqRpt: + type: integer + nullable: true + description: Discipline sequence in reports + ResultDateTime: + type: string + format: date-time + description: Result timestamp + CreateDate: + type: string + format: date-time + PatientPatch: + type: object + description: | + Partial patient update payload. + Omitted fields are left unchanged. Send null explicitly to clear nullable fields. + properties: + PatientID: + type: string + maxLength: 30 + pattern: ^[A-Za-z0-9]+$ + description: Internal patient identifier + AlternatePID: + type: string + maxLength: 30 + pattern: ^[A-Za-z0-9]+$ + Prefix: + type: string + maxLength: 10 + enum: + - Mr + - Mrs + - Ms + - Dr + - Prof + Sex: + type: string + enum: + - '1' + - '2' + description: '1: Female, 2: Male' + NameFirst: + type: string + minLength: 1 + maxLength: 60 + pattern: ^[A-Za-z'\. ]+$ + NameMiddle: + type: string + minLength: 1 + maxLength: 60 + NameMaiden: + type: string + minLength: 1 + maxLength: 60 + NameLast: + type: string + minLength: 1 + maxLength: 60 + Suffix: + type: string + maxLength: 10 + Birthdate: + type: string + format: date-time + description: ISO 8601 UTC datetime + PlaceOfBirth: + type: string + maxLength: 100 + Citizenship: + type: string + maxLength: 100 + Street_1: + type: string + maxLength: 255 + Street_2: + type: string + maxLength: 255 + Street_3: + type: string + maxLength: 255 + ZIP: + type: string + maxLength: 10 + pattern: ^[0-9]+$ + Phone: + type: string + pattern: ^\+?[0-9]{8,15}$ + MobilePhone: + type: string + pattern: ^\+?[0-9]{8,15}$ + EmailAddress1: + type: string + format: email + maxLength: 100 + EmailAddress2: + type: string + format: email + maxLength: 100 + PatIdt: + allOf: + - $ref: '#/components/schemas/PatientIdentifier' + nullable: true + LinkTo: + type: array + description: Array of linked patient references + items: + $ref: '#/components/schemas/LinkedPatient' + Custodian: + allOf: + - $ref: '#/components/schemas/Custodian' + nullable: true + isDead: + type: string + enum: + - '0' + - '1' + description: '0: No (alive), 1: Yes (deceased)' + TimeOfDeath: + type: string + format: date-time + description: ISO 8601 UTC datetime of death + PatCom: + type: string + description: Patient comment/notes + nullable: true + PatAtt: + type: array + description: Patient address entries + nullable: true + items: + $ref: '#/components/schemas/PatAttEntry' + Province: + type: integer + description: Province AreaGeoID (foreign key to areageo table) + City: + type: integer + description: City AreaGeoID (foreign key to areageo table) + Country: + type: string + maxLength: 10 + description: Country ISO 3-letter code (e.g., IDN, USA) + Race: + type: string + maxLength: 100 + MaritalStatus: + type: string + enum: + - A + - B + - D + - M + - S + - W + description: 'A: Annulled, B: Separated, D: Divorced, M: Married, S: Single, W: Widowed' + Religion: + type: string + maxLength: 100 + Ethnic: + type: string + maxLength: 100 + TestMapDetail: + type: object + properties: + TestMapDetailID: + type: integer + TestMapID: + type: integer + HostTestCode: + type: string + description: Test code in host system + HostTestName: + type: string + description: Test name in host system + ConDefID: + type: integer + description: Container definition ID + ContainerLabel: + type: string + description: Container definition name + ClientTestCode: + type: string + description: Test code in client system + ClientTestName: + type: string + description: Test name in client system + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + description: Soft delete timestamp + TestDefinitionListItem: + type: object + properties: + TestSiteID: + type: integer + TestSiteCode: + type: string + TestSiteName: + type: string + TestType: + type: string + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + SeqScr: + type: integer + SeqRpt: + type: integer + isVisibleScr: + type: integer + enum: + - 0 + - 1 + isVisibleRpt: + type: integer + enum: + - 0 + - 1 + isCountStat: + type: integer + StartDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + nullable: true + DisciplineID: + type: integer + nullable: true + DepartmentID: + type: integer + nullable: true + DisciplineName: + type: string + nullable: true + DepartmentName: + type: string + nullable: true + ValueSetListItem: + type: object + description: Library/system value set summary (from JSON files) + properties: + value: + type: string + description: The value set key/name + label: + type: string + description: The display name/label + count: + type: integer + description: Number of items in this value set diff --git a/public/components/schemas/audit-logs.yaml b/public/components/schemas/audit-logs.yaml index c3a6eeb..384e502 100755 --- a/public/components/schemas/audit-logs.yaml +++ b/public/components/schemas/audit-logs.yaml @@ -1,107 +1,107 @@ -AuditLogEntry: - type: object - properties: - LogPatientID: - type: integer - nullable: true - LogOrderID: - type: integer - nullable: true - LogMasterID: - type: integer - nullable: true - LogSystemID: - type: integer - nullable: true - TblName: - type: string - RecID: - type: string - FldName: - type: string - nullable: true - FldValuePrev: - type: string - nullable: true - FldValueNew: - type: string - nullable: true - UserID: - type: string - SiteID: - type: string - DIDType: - type: string - nullable: true - DID: - type: string - nullable: true - MachineID: - type: string - nullable: true - SessionID: - type: string - AppID: - type: string - ProcessID: - type: string - nullable: true - WebPageID: - type: string - nullable: true - EventID: - type: string - ActivityID: - type: string - Reason: - type: string - nullable: true - LogDate: - type: string - format: date-time - Context: - type: string - IpAddress: - type: string - nullable: true - -AuditLogListResponse: - type: object - properties: - data: - type: array - items: - $ref: '#/AuditLogEntry' - pagination: - type: object - properties: - page: - type: integer - perPage: - type: integer - total: - type: integer - required: [page, perPage, total] - required: [data, pagination] - -AuditLogsEnvelope: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/AuditLogListResponse' - required: [status, message, data] - -AuditLogsErrorResponse: - type: object - properties: - status: - type: string - message: - type: string - data: - nullable: true - required: [status, message, data] +AuditLogEntry: + type: object + properties: + LogPatientID: + type: integer + nullable: true + LogOrderID: + type: integer + nullable: true + LogMasterID: + type: integer + nullable: true + LogSystemID: + type: integer + nullable: true + TblName: + type: string + RecID: + type: string + FldName: + type: string + nullable: true + FldValuePrev: + type: string + nullable: true + FldValueNew: + type: string + nullable: true + UserID: + type: string + SiteID: + type: string + DIDType: + type: string + nullable: true + DID: + type: string + nullable: true + MachineID: + type: string + nullable: true + SessionID: + type: string + AppID: + type: string + ProcessID: + type: string + nullable: true + WebPageID: + type: string + nullable: true + EventID: + type: string + ActivityID: + type: string + Reason: + type: string + nullable: true + LogDate: + type: string + format: date-time + Context: + type: string + IpAddress: + type: string + nullable: true + +AuditLogListResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/AuditLogEntry' + pagination: + type: object + properties: + page: + type: integer + perPage: + type: integer + total: + type: integer + required: [page, perPage, total] + required: [data, pagination] + +AuditLogsEnvelope: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/AuditLogListResponse' + required: [status, message, data] + +AuditLogsErrorResponse: + type: object + properties: + status: + type: string + message: + type: string + data: + nullable: true + required: [status, message, data] diff --git a/public/components/schemas/user.yaml b/public/components/schemas/user.yaml index f9aee2d..f774d1b 100755 --- a/public/components/schemas/user.yaml +++ b/public/components/schemas/user.yaml @@ -1,121 +1,121 @@ -User: - type: object - properties: - UserID: - type: integer - description: Unique user identifier - Username: - type: string - description: Unique login username - Email: - type: string - format: email - description: User email address - Name: - type: string - description: Full name of the user - Role: - type: string - description: User role (admin, technician, doctor, etc.) - Department: - type: string - description: Department name - IsActive: - type: boolean - description: Whether the user account is active - CreatedAt: - type: string - format: date-time - description: Creation timestamp - UpdatedAt: - type: string - format: date-time - description: Last update timestamp - DelDate: - type: string - format: date-time - nullable: true - description: Soft delete timestamp (null if active) - -UserCreate: - type: object - required: - - Username - - Email - properties: - Username: - type: string - minLength: 3 - maxLength: 50 - description: Unique login username - Email: - type: string - format: email - maxLength: 100 - description: User email address - Name: - type: string - description: Full name of the user - Role: - type: string - description: User role - Department: - type: string - description: Department name - IsActive: - type: boolean - default: true - description: Whether the user account is active - -UserUpdate: - type: object - required: - - UserID - properties: - UserID: - type: integer - description: User ID to update - Email: - type: string - format: email - description: User email address - Name: - type: string - description: Full name of the user - Role: - type: string - description: User role - Department: - type: string - description: Department name - IsActive: - type: boolean - description: Whether the user account is active - -UserListResponse: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Users retrieved successfully - data: - type: object - properties: - users: - type: array - items: - $ref: '#/User' - pagination: - type: object - properties: - current_page: - type: integer - per_page: - type: integer - total: - type: integer - total_pages: +User: + type: object + properties: + UserID: + type: integer + description: Unique user identifier + Username: + type: string + description: Unique login username + Email: + type: string + format: email + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role (admin, technician, doctor, etc.) + Department: + type: string + description: Department name + IsActive: + type: boolean + description: Whether the user account is active + CreatedAt: + type: string + format: date-time + description: Creation timestamp + UpdatedAt: + type: string + format: date-time + description: Last update timestamp + DelDate: + type: string + format: date-time + nullable: true + description: Soft delete timestamp (null if active) + +UserCreate: + type: object + required: + - Username + - Email + properties: + Username: + type: string + minLength: 3 + maxLength: 50 + description: Unique login username + Email: + type: string + format: email + maxLength: 100 + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role + Department: + type: string + description: Department name + IsActive: + type: boolean + default: true + description: Whether the user account is active + +UserUpdate: + type: object + required: + - UserID + properties: + UserID: + type: integer + description: User ID to update + Email: + type: string + format: email + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role + Department: + type: string + description: Department name + IsActive: + type: boolean + description: Whether the user account is active + +UserListResponse: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Users retrieved successfully + data: + type: object + properties: + users: + type: array + items: + $ref: '#/User' + pagination: + type: object + properties: + current_page: + type: integer + per_page: + type: integer + total: + type: integer + total_pages: type: integer \ No newline at end of file diff --git a/public/paths/audit-logs.yaml b/public/paths/audit-logs.yaml index db0a7ff..2f94e08 100755 --- a/public/paths/audit-logs.yaml +++ b/public/paths/audit-logs.yaml @@ -1,76 +1,76 @@ -/api/audit-logs: - get: - tags: [Audit] - summary: Retrieve audit log entries for a table - security: - - bearerAuth: [] - parameters: - - name: table - in: query - required: true - schema: - type: string - description: Table alias for the audit data (logpatient, logorder, logmaster, logsystem) - - name: rec_id - in: query - schema: - type: string - description: Primary record identifier (RecID) to filter audit rows - - name: event_id - in: query - schema: - type: string - description: Canonical EventID (case insensitive) - - name: activity_id - in: query - schema: - type: string - description: Canonical ActivityID (case insensitive) - - name: from - in: query - schema: - type: string - format: date-time - description: Lower bound for LogDate inclusive - - name: to - in: query - schema: - type: string - format: date-time - description: Upper bound for LogDate inclusive - - name: search - in: query - schema: - type: string - description: Search term that matches user, reason, field names, or values - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number - - name: perPage - in: query - schema: - type: integer - default: 20 - description: Items per page (max 100) - responses: - '200': - description: Audit log results - content: - application/json: - schema: - $ref: '../components/schemas/audit-logs.yaml#/AuditLogsEnvelope' - '400': - description: Validation failure (missing table or invalid filters) - content: - application/json: - schema: - $ref: '../components/schemas/audit-logs.yaml#/AuditLogsErrorResponse' - '500': - description: Internal error when retrieving audit logs - content: - application/json: - schema: - $ref: '../components/schemas/audit-logs.yaml#/AuditLogsErrorResponse' +/api/audit-logs: + get: + tags: [Audit] + summary: Retrieve audit log entries for a table + security: + - bearerAuth: [] + parameters: + - name: table + in: query + required: true + schema: + type: string + description: Table alias for the audit data (logpatient, logorder, logmaster, logsystem) + - name: rec_id + in: query + schema: + type: string + description: Primary record identifier (RecID) to filter audit rows + - name: event_id + in: query + schema: + type: string + description: Canonical EventID (case insensitive) + - name: activity_id + in: query + schema: + type: string + description: Canonical ActivityID (case insensitive) + - name: from + in: query + schema: + type: string + format: date-time + description: Lower bound for LogDate inclusive + - name: to + in: query + schema: + type: string + format: date-time + description: Upper bound for LogDate inclusive + - name: search + in: query + schema: + type: string + description: Search term that matches user, reason, field names, or values + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number + - name: perPage + in: query + schema: + type: integer + default: 20 + description: Items per page (max 100) + responses: + '200': + description: Audit log results + content: + application/json: + schema: + $ref: '../components/schemas/audit-logs.yaml#/AuditLogsEnvelope' + '400': + description: Validation failure (missing table or invalid filters) + content: + application/json: + schema: + $ref: '../components/schemas/audit-logs.yaml#/AuditLogsErrorResponse' + '500': + description: Internal error when retrieving audit logs + content: + application/json: + schema: + $ref: '../components/schemas/audit-logs.yaml#/AuditLogsErrorResponse' diff --git a/public/paths/calc.yaml b/public/paths/calc.yaml index c46de54..8372589 100755 --- a/public/paths/calc.yaml +++ b/public/paths/calc.yaml @@ -1,112 +1,112 @@ -/api/calc/testcode/{codeOrName}: - post: - tags: [Calculation] - summary: Evaluate a configured calculation by test code or name and return the raw result map. - security: [] - parameters: - - name: codeOrName - in: path - required: true - schema: - type: string - description: TestSiteCode or TestSiteName of the calculated test (case-insensitive). - requestBody: - required: true - content: - application/json: - schema: - type: object - description: Key-value pairs where keys match member tests used in the formula. - additionalProperties: - type: number - example: - TBIL: 5 - DBIL: 3 - responses: - '200': - description: Returns a single key/value pair with the canonical TestSiteCode or an empty object when the calculation is incomplete or missing. - content: - application/json: - schema: - type: object - examples: - success: - value: - 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 +/api/calc/testcode/{codeOrName}: + post: + tags: [Calculation] + summary: Evaluate a configured calculation by test code or name and return the raw result map. + security: [] + parameters: + - name: codeOrName + in: path + required: true + schema: + type: string + description: TestSiteCode or TestSiteName of the calculated test (case-insensitive). + requestBody: + required: true + content: + application/json: + schema: + type: object + description: Key-value pairs where keys match member tests used in the formula. + additionalProperties: + type: number + example: + TBIL: 5 + DBIL: 3 + responses: + '200': + description: Returns a single key/value pair with the canonical TestSiteCode or an empty object when the calculation is incomplete or missing. + content: + application/json: + schema: + type: object + examples: + success: + value: + 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 diff --git a/public/paths/users.yaml b/public/paths/users.yaml index 8910a79..1ee5126 100755 --- a/public/paths/users.yaml +++ b/public/paths/users.yaml @@ -1,220 +1,220 @@ -/api/user: - get: - tags: [User] - summary: List users with pagination and search - security: - - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number - - name: per_page - in: query - schema: - type: integer - default: 20 - description: Items per page - - name: search - in: query - schema: - type: string - description: Search term for username, email, or name - responses: - '200': - description: List of users with pagination - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: Users retrieved successfully - data: - type: object - properties: - users: - type: array - items: - $ref: '../components/schemas/user.yaml#/User' - pagination: - type: object - properties: - current_page: - type: integer - per_page: - type: integer - total: - type: integer - total_pages: - type: integer - '500': - description: Server error - - post: - tags: [User] - summary: Create new user - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '../components/schemas/user.yaml#/UserCreate' - responses: - '201': - description: User created successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: User created successfully - data: - type: object - properties: - UserID: - type: integer - Username: - type: string - Email: - type: string - '400': - description: Validation failed - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: failed - message: - type: string - example: Validation failed - data: - type: object - '500': - description: Server error - - -/api/user/{id}: - get: - tags: [User] - summary: Get user by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: User ID - responses: - '200': - description: User details - content: - application/json: - schema: - $ref: '../components/schemas/user.yaml#/User' - '404': - description: User not found - '500': - description: Server error - - patch: - tags: [User] - summary: Update existing user - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: User ID - requestBody: - required: true - content: - application/json: - schema: - $ref: '../components/schemas/user.yaml#/UserUpdate' - responses: - '200': - description: User updated successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: User updated successfully - data: - type: object - properties: - UserID: - type: integer - updated_fields: - type: array - items: - type: string - '400': - description: UserID is required - '404': - description: User not found - '500': - description: Server error - - delete: - tags: [User] - summary: Delete user (soft delete) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: User ID - responses: - '200': - description: User deleted successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - example: User deleted successfully - data: - type: object - properties: - UserID: - type: integer - '404': - description: User not found - '500': - description: Server error +/api/user: + get: + tags: [User] + summary: List users with pagination and search + security: + - bearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number + - name: per_page + in: query + schema: + type: integer + default: 20 + description: Items per page + - name: search + in: query + schema: + type: string + description: Search term for username, email, or name + responses: + '200': + description: List of users with pagination + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Users retrieved successfully + data: + type: object + properties: + users: + type: array + items: + $ref: '../components/schemas/user.yaml#/User' + pagination: + type: object + properties: + current_page: + type: integer + per_page: + type: integer + total: + type: integer + total_pages: + type: integer + '500': + description: Server error + + post: + tags: [User] + summary: Create new user + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/user.yaml#/UserCreate' + responses: + '201': + description: User created successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User created successfully + data: + type: object + properties: + UserID: + type: integer + Username: + type: string + Email: + type: string + '400': + description: Validation failed + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: Validation failed + data: + type: object + '500': + description: Server error + + +/api/user/{id}: + get: + tags: [User] + summary: Get user by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User details + content: + application/json: + schema: + $ref: '../components/schemas/user.yaml#/User' + '404': + description: User not found + '500': + description: Server error + + patch: + tags: [User] + summary: Update existing user + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/user.yaml#/UserUpdate' + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User updated successfully + data: + type: object + properties: + UserID: + type: integer + updated_fields: + type: array + items: + type: string + '400': + description: UserID is required + '404': + description: User not found + '500': + description: Server error + + delete: + tags: [User] + summary: Delete user (soft delete) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User deleted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User deleted successfully + data: + type: object + properties: + UserID: + type: integer + '404': + description: User not found + '500': + description: Server error diff --git a/tests/_support/Traits/CreatesPatients.php b/tests/_support/Traits/CreatesPatients.php index 13969e2..b28f407 100755 --- a/tests/_support/Traits/CreatesPatients.php +++ b/tests/_support/Traits/CreatesPatients.php @@ -1,62 +1,62 @@ - 'PAT' . $faker->numerify('##########'), - 'AlternatePID' => 'ALT' . $faker->numerify('##########'), - 'Prefix' => $faker->title, - 'NameFirst' => 'Test', - 'NameMiddle' => $faker->firstName, - 'NameLast' => 'Patient', - 'Suffix' => 'S.Kom', - 'Sex' => (string) $faker->numberBetween(5, 6), - 'PlaceOfBirth' => $faker->city, - 'Birthdate' => $faker->date('Y-m-d'), - 'ZIP' => $faker->postcode, - 'Street_1' => $faker->streetAddress, - 'City' => $faker->city, - 'Province' => $faker->state, - 'EmailAddress1' => 'test.' . $faker->unique()->userName . '@example.com', - 'Phone' => $faker->numerify('08##########'), - 'MobilePhone' => $faker->numerify('08##########'), - 'Race' => (string) $faker->numberBetween(175, 205), - 'Country' => (string) $faker->numberBetween(221, 469), - 'MaritalStatus' => (string) $faker->numberBetween(8, 15), - 'Religion' => (string) $faker->numberBetween(206, 212), - 'Ethnic' => (string) $faker->numberBetween(213, 220), - 'Citizenship' => 'WNI', - 'isDead' => (string) $faker->numberBetween(0, 1), - 'PatIdt' => [ - 'IdentifierType' => 'ID', - 'Identifier' => $faker->numerify('################') - ], - 'PatAtt' => [ - [ 'Address' => '/api/upload/' . $faker->uuid . '.jpg' ] - ], - 'PatCom' => $faker->sentence, - ], $overrides); - - if ($patientPayload['isDead'] === '1') { - $patientPayload['DeathDateTime'] = $faker->date('Y-m-d H:i:s'); - } else { - $patientPayload['DeathDateTime'] = null; - } - - $patientModel = new PatientModel(); - $internalPID = $patientModel->createPatient($patientPayload); - if (!$internalPID) { - throw new \RuntimeException('Failed to insert test patient'); - } - - return $internalPID; - } -} + 'PAT' . $faker->numerify('##########'), + 'AlternatePID' => 'ALT' . $faker->numerify('##########'), + 'Prefix' => $faker->title, + 'NameFirst' => 'Test', + 'NameMiddle' => $faker->firstName, + 'NameLast' => 'Patient', + 'Suffix' => 'S.Kom', + 'Sex' => (string) $faker->numberBetween(5, 6), + 'PlaceOfBirth' => $faker->city, + 'Birthdate' => $faker->date('Y-m-d'), + 'ZIP' => $faker->postcode, + 'Street_1' => $faker->streetAddress, + 'City' => $faker->city, + 'Province' => $faker->state, + 'EmailAddress1' => 'test.' . $faker->unique()->userName . '@example.com', + 'Phone' => $faker->numerify('08##########'), + 'MobilePhone' => $faker->numerify('08##########'), + 'Race' => (string) $faker->numberBetween(175, 205), + 'Country' => (string) $faker->numberBetween(221, 469), + 'MaritalStatus' => (string) $faker->numberBetween(8, 15), + 'Religion' => (string) $faker->numberBetween(206, 212), + 'Ethnic' => (string) $faker->numberBetween(213, 220), + 'Citizenship' => 'WNI', + 'isDead' => (string) $faker->numberBetween(0, 1), + 'PatIdt' => [ + 'IdentifierType' => 'ID', + 'Identifier' => $faker->numerify('################') + ], + 'PatAtt' => [ + [ 'Address' => '/api/upload/' . $faker->uuid . '.jpg' ] + ], + 'PatCom' => $faker->sentence, + ], $overrides); + + if ($patientPayload['isDead'] === '1') { + $patientPayload['DeathDateTime'] = $faker->date('Y-m-d H:i:s'); + } else { + $patientPayload['DeathDateTime'] = null; + } + + $patientModel = new PatientModel(); + $internalPID = $patientModel->createPatient($patientPayload); + if (!$internalPID) { + throw new \RuntimeException('Failed to insert test patient'); + } + + return $internalPID; + } +} diff --git a/tests/feature/Audit/AuditLogTest.php b/tests/feature/Audit/AuditLogTest.php index 8069692..14f1eab 100755 --- a/tests/feature/Audit/AuditLogTest.php +++ b/tests/feature/Audit/AuditLogTest.php @@ -1,117 +1,117 @@ -db = \Config\Database::connect(); - $this->db->table('logpatient')->insert([ - 'TblName' => 'patient', - 'RecID' => $this->testRecId, - 'UserID' => 'USR_TEST', - 'SiteID' => 'SITE01', - 'SessionID' => 'sess_test', - 'AppID' => 'clqms-api', - 'EventID' => 'PATIENT_REGISTERED', - 'ActivityID' => 'CREATE', - 'LogDate' => '2026-03-25 12:00:00', - 'Context' => json_encode([ - 'request_id' => 'req-test-1', - 'route' => 'POST /api/patient', - 'timestamp_utc' => '2026-03-25T12:00:00.000Z', - 'entity_type' => 'patient', - 'entity_version' => 1, - ]), - ]); - } - - protected function tearDown(): void - { - $this->db->table('logpatient')->where('RecID', $this->testRecId)->delete(); - parent::tearDown(); - } - - public function testTableIsRequired() - { - $result = $this->getWithAuth('api/audit-logs'); - - $result->assertStatus(400); - $result->assertJSONFragment([ - 'status' => 'failed', - 'message' => 'table parameter is required', - ]); - } - - public function testUnknownTableReturnsValidationError() - { - $result = $this->getWithAuth('api/audit-logs?table=unknown'); - - $result->assertStatus(400); - $result->assertJSONFragment([ - 'status' => 'failed', - 'message' => 'Unknown audit table: unknown', - ]); - } - - public function testAuditLogsFilterByRecId() - { - $result = $this->getWithAuth('api/audit-logs?table=logpatient&rec_id=' . $this->testRecId); - - $result->assertStatus(200); - $result->assertJSONFragment([ - 'status' => 'success', - ]); - - $payload = json_decode($result->getJSON(), true); - $this->assertCount(1, $payload['data']['data']); - $this->assertEquals($this->testRecId, $payload['data']['data'][0]['RecID']); - - $pagination = $payload['data']['pagination']; - $this->assertSame(1, $pagination['page']); - $this->assertSame(20, $pagination['perPage']); - $this->assertSame(1, $pagination['total']); - } - - private function getWithAuth(string $uri) - { - $_COOKIE['token'] = $this->buildToken(); - - $response = $this->get($uri); - - unset($_COOKIE['token']); - - return $response; - } - - private function buildToken(): string - { - $payload = [ - 'sub' => 'audit-test', - 'iat' => time(), - ]; - - return JWT::encode($payload, $this->resolveSecret(), 'HS256'); - } - - private function resolveSecret(): string - { - $secret = getenv('JWT_SECRET'); - if ($secret === false) { - return 'tests-secret'; - } - return trim($secret, "'\""); - } -} +db = \Config\Database::connect(); + $this->db->table('logpatient')->insert([ + 'TblName' => 'patient', + 'RecID' => $this->testRecId, + 'UserID' => 'USR_TEST', + 'SiteID' => 'SITE01', + 'SessionID' => 'sess_test', + 'AppID' => 'clqms-api', + 'EventID' => 'PATIENT_REGISTERED', + 'ActivityID' => 'CREATE', + 'LogDate' => '2026-03-25 12:00:00', + 'Context' => json_encode([ + 'request_id' => 'req-test-1', + 'route' => 'POST /api/patient', + 'timestamp_utc' => '2026-03-25T12:00:00.000Z', + 'entity_type' => 'patient', + 'entity_version' => 1, + ]), + ]); + } + + protected function tearDown(): void + { + $this->db->table('logpatient')->where('RecID', $this->testRecId)->delete(); + parent::tearDown(); + } + + public function testTableIsRequired() + { + $result = $this->getWithAuth('api/audit-logs'); + + $result->assertStatus(400); + $result->assertJSONFragment([ + 'status' => 'failed', + 'message' => 'table parameter is required', + ]); + } + + public function testUnknownTableReturnsValidationError() + { + $result = $this->getWithAuth('api/audit-logs?table=unknown'); + + $result->assertStatus(400); + $result->assertJSONFragment([ + 'status' => 'failed', + 'message' => 'Unknown audit table: unknown', + ]); + } + + public function testAuditLogsFilterByRecId() + { + $result = $this->getWithAuth('api/audit-logs?table=logpatient&rec_id=' . $this->testRecId); + + $result->assertStatus(200); + $result->assertJSONFragment([ + 'status' => 'success', + ]); + + $payload = json_decode($result->getJSON(), true); + $this->assertCount(1, $payload['data']['data']); + $this->assertEquals($this->testRecId, $payload['data']['data'][0]['RecID']); + + $pagination = $payload['data']['pagination']; + $this->assertSame(1, $pagination['page']); + $this->assertSame(20, $pagination['perPage']); + $this->assertSame(1, $pagination['total']); + } + + private function getWithAuth(string $uri) + { + $_COOKIE['token'] = $this->buildToken(); + + $response = $this->get($uri); + + unset($_COOKIE['token']); + + return $response; + } + + private function buildToken(): string + { + $payload = [ + 'sub' => 'audit-test', + 'iat' => time(), + ]; + + return JWT::encode($payload, $this->resolveSecret(), 'HS256'); + } + + private function resolveSecret(): string + { + $secret = getenv('JWT_SECRET'); + if ($secret === false) { + return 'tests-secret'; + } + return trim($secret, "'\""); + } +} diff --git a/tests/feature/Contact/ContactPatchTest.php b/tests/feature/Contact/ContactPatchTest.php index 541ab55..a0b4ac5 100755 --- a/tests/feature/Contact/ContactPatchTest.php +++ b/tests/feature/Contact/ContactPatchTest.php @@ -156,6 +156,7 @@ class ContactPatchTest extends CIUnitTestCase [ 'ContactDetID' => $keepDetail['ContactDetID'], 'JobTitle' => 'Senior Doctor', + 'ContactStartDate' => '2026-04-16T07:22:44.000Z', ], ], 'created' => [ @@ -182,8 +183,19 @@ class ContactPatchTest extends CIUnitTestCase $this->assertCount(2, $afterData['Details']); $detailIds = array_column($afterData['Details'], 'ContactDetID'); $this->assertContains($keepDetail['ContactDetID'], $detailIds); + $this->assertNotContains($deleteDetail['ContactDetID'], $detailIds); - $updatedDetails = array_values(array_filter($afterData['Details'], static fn ($row) => $row['ContactDetID'] === $keepDetail['ContactDetID'])); + $updatedDetails = array_values(array_filter( + $afterData['Details'], + static fn ($row) => $row['ContactDetID'] === $keepDetail['ContactDetID'] + )); $this->assertNotEmpty($updatedDetails); + $this->assertSame('Senior Doctor', $updatedDetails[0]['JobTitle']); + + $createdDetails = array_values(array_filter( + $afterData['Details'], + static fn ($row) => (string) $row['SiteID'] === '3' + )); + $this->assertNotEmpty($createdDetails); } } diff --git a/tests/feature/Location/LocationPatchTest.php b/tests/feature/Location/LocationPatchTest.php index 852be95..5646f2b 100755 --- a/tests/feature/Location/LocationPatchTest.php +++ b/tests/feature/Location/LocationPatchTest.php @@ -1,144 +1,144 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createLocation(array $data = []): array - { - $payload = array_merge([ - 'LocCode' => 'LC' . substr(uniqid(), -4), - 'LocFull' => 'Test Location ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - $locationId = $decoded['data']['LocationID']; - $show = $this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$locationId}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - return $showData; - } - - public function testPartialUpdateLocationSuccess() - { - $location = $this->createLocation(); - $id = $location['LocationID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['LocFull' => 'Updated Location']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Location', $showData['LocFull']); - $this->assertEquals($location['LocCode'], $showData['LocCode']); - } - - public function testPartialUpdateLocationNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['LocFull' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateLocationInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['LocFull' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateLocationEmptyPayload() - { - $location = $this->createLocation(); - $id = $location['LocationID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateLocationSingleField() - { - $location = $this->createLocation(); - $id = $location['LocationID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['LocCode' => 'LC' . substr(uniqid(), -4)]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($location['LocCode'], $showData['LocCode']); - $this->assertEquals($location['LocFull'], $showData['LocFull']); - - } - - public function testPartialUpdateLocationAddressField() - { - $location = $this->createLocation(); - $id = $location['LocationID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['Street1' => '123 Market St']); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertEquals('123 Market St', $showData['Street1']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createLocation(array $data = []): array + { + $payload = array_merge([ + 'LocCode' => 'LC' . substr(uniqid(), -4), + 'LocFull' => 'Test Location ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + $locationId = $decoded['data']['LocationID']; + $show = $this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$locationId}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + return $showData; + } + + public function testPartialUpdateLocationSuccess() + { + $location = $this->createLocation(); + $id = $location['LocationID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['LocFull' => 'Updated Location']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Location', $showData['LocFull']); + $this->assertEquals($location['LocCode'], $showData['LocCode']); + } + + public function testPartialUpdateLocationNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['LocFull' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateLocationInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['LocFull' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateLocationEmptyPayload() + { + $location = $this->createLocation(); + $id = $location['LocationID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateLocationSingleField() + { + $location = $this->createLocation(); + $id = $location['LocationID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['LocCode' => 'LC' . substr(uniqid(), -4)]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($location['LocCode'], $showData['LocCode']); + $this->assertEquals($location['LocFull'], $showData['LocFull']); + + } + + public function testPartialUpdateLocationAddressField() + { + $location = $this->createLocation(); + $id = $location['LocationID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['Street1' => '123 Market St']); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertEquals('123 Market St', $showData['Street1']); + } +} diff --git a/tests/feature/MasterDataPatchTest.php b/tests/feature/MasterDataPatchTest.php index 0dd1bef..8ff3923 100644 --- a/tests/feature/MasterDataPatchTest.php +++ b/tests/feature/MasterDataPatchTest.php @@ -1,301 +1,301 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createResource(string $endpoint, array $payload) - { - $response = $this->withHeaders($this->authHeaders()) - ->withBody(json_encode($payload)) - ->call('post', $endpoint); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - - $this->assertEquals('success', $decoded['status']); - - return $decoded['data']; - } - - private function fetchFirstRecord(string $endpoint, string $idKey): array - { - try { - $response = $this->withHeaders($this->authHeaders())->call('get', $endpoint); - } catch (PageNotFoundException $e) { - $this->markTestSkipped("{$endpoint} GET not available: {$e->getMessage()}"); - } - - $response->assertStatus(200); - $decoded = json_decode($response->getJSON(), true); - $rows = $decoded['data'] ?? []; - - if (empty($rows)) { - $this->markTestSkipped("No data available at {$endpoint}"); - } - - $record = $rows[0]; - $this->assertArrayHasKey($idKey, $record); - - return $record; - } - - private function fetchResource(string $endpoint, $id): array - { - try { - $response = $this->withHeaders($this->authHeaders()) - ->call('get', "$endpoint/{$id}"); - } catch (PageNotFoundException $e) { - $this->markTestSkipped("{$endpoint}/{$id} GET not available: {$e->getMessage()}"); - } - - $response->assertStatus(200); - $decoded = json_decode($response->getJSON(), true); - - return $decoded['data']; - } - - public function testPartialUpdateOccupation() - { - $occCode = 'PATCH_OCC_' . uniqid(); - $id = $this->createResource('api/occupation', [ - 'OccCode' => $occCode, - 'OccText' => 'Original text', - ]); - - $originalData = $this->fetchResource('api/occupation', $id); - $originalCode = $originalData['OccCode']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/occupation/{$id}", ['OccText' => 'Patched occupation']); - - $patch->assertStatus(201); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $showData = $this->fetchResource('api/occupation', $id); - - $this->assertEquals('Patched occupation', $showData['OccText']); - $this->assertEquals($originalCode, $showData['OccCode']); - } - - public function testPartialUpdateMedicalSpecialty() - { - $text = 'Specialty ' . uniqid(); - $id = $this->createResource('api/medicalspecialty', ['SpecialtyText' => $text]); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/medicalspecialty/{$id}", ['SpecialtyText' => 'Updated specialty']); - - $patch->assertStatus(201); - $show = $this->withHeaders($this->authHeaders())->call('get', "api/medicalspecialty/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated specialty', $showData['SpecialtyText']); - } - - public function testPartialUpdateCounter() - { - $initial = 'Counter ' . uniqid(); - $id = $this->createResource('api/counter', [ - 'CounterName' => $initial, - 'CounterValue' => 1, - 'CounterStart' => 1, - 'CounterEnd' => 10, - 'CounterReset' => 1, - ]); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/counter/{$id}", ['CounterName' => 'Updated counter']); - - $patch->assertStatus(201); - $show = $this->withHeaders($this->authHeaders())->call('get', "api/counter/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated counter', $showData['CounterName']); - $this->assertEquals(1, (int) $showData['CounterValue']); - } - - public function testPartialUpdateOrganizationAccount() - { - $name = 'Account ' . uniqid(); - $id = $this->createResource('api/organization/account', ['AccountName' => $name]); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/organization/account/{$id}", ['AccountName' => 'Updated account']); - - $patch->assertStatus(200); - $show = $this->withHeaders($this->authHeaders())->call('get', "api/organization/account/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated account', $showData['AccountName']); - } - - public function testPartialUpdateDiscipline() - { - $code = 'DIS_' . strtoupper(bin2hex(random_bytes(2))); - $name = 'Discipline ' . uniqid(); - $id = $this->createResource('api/organization/discipline', [ - 'DisciplineCode' => $code, - 'DisciplineName' => $name, - ]); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/organization/discipline/{$id}", ['DisciplineName' => 'Discipline Updated']); - - $patch->assertStatus(201); - $show = $this->withHeaders($this->authHeaders())->call('get', "api/organization/discipline/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Discipline Updated', $showData['DisciplineName']); - $this->assertEquals($code, $showData['DisciplineCode']); - } - - public function testPartialUpdateCodingSystem() - { - $abbr = 'CS' . strtoupper(bin2hex(random_bytes(2))); - $full = 'Full text ' . uniqid(); - $id = $this->createResource('api/organization/codingsys', [ - 'CodingSysAbb' => $abbr, - 'FullText' => $full, - ]); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/organization/codingsys/{$id}", ['FullText' => 'Updated full text']); - - $patch->assertStatus(201); - $show = $this->withHeaders($this->authHeaders())->call('get', "api/organization/codingsys/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated full text', $showData['FullText']); - $this->assertEquals($abbr, $showData['CodingSysAbb']); - } - - public function testPartialUpdateSpecimenContainer() - { - $record = $this->fetchFirstRecord('api/specimen/container', 'ConDefID'); - $id = $record['ConDefID']; - $newName = 'Patch Container ' . uniqid(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/specimen/container/{$id}", ['ConName' => $newName]); - - $patch->assertStatus(201); - $showData = $this->fetchResource('api/specimen/container', $id); - - $this->assertEquals($newName, $showData['ConName']); - $this->assertEquals($record['ConCode'] ?? null, $showData['ConCode'] ?? null); - } - - public function testPartialUpdateSpecimenPrep() - { - $record = $this->fetchFirstRecord('api/specimen/prep', 'SpcPrpID'); - $id = $record['SpcPrpID']; - $newDesc = 'Partial Prep ' . uniqid(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/specimen/prep/{$id}", ['Description' => $newDesc]); - - $patch->assertStatus(201); - $showData = $this->fetchResource('api/specimen/prep', $id); - - $this->assertEquals($newDesc, $showData['Description']); - $this->assertEquals($record['SpcStaID'] ?? null, $showData['SpcStaID'] ?? null); - } - - public function testPartialUpdateSpecimenStatus() - { - $record = $this->fetchFirstRecord('api/specimen/status', 'SpcStaID'); - $id = $record['SpcStaID']; - $newStatus = 'UpdatedStatus'; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/specimen/status/{$id}", ['SpcStatus' => $newStatus]); - - $patch->assertStatus(201); - $showData = $this->fetchResource('api/specimen/status', $id); - - $this->assertEquals($newStatus, $showData['SpcStatus']); - $this->assertEquals($record['OrderID'] ?? null, $showData['OrderID'] ?? null); - } - - public function testPartialUpdateSpecimenCollection() - { - $record = $this->fetchFirstRecord('api/specimen/collection', 'SpcColID'); - $id = $record['SpcColID']; - $newBodySite = 'BodySite ' . uniqid(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/specimen/collection/{$id}", ['BodySite' => $newBodySite]); - - $patch->assertStatus(201); - $showData = $this->fetchResource('api/specimen/collection', $id); - - $this->assertEquals($newBodySite, $showData['BodySite']); - $this->assertEquals($record['SpRole'] ?? null, $showData['SpRole'] ?? null); - } - - public function testPartialUpdateEquipmentList() - { - $record = $this->fetchFirstRecord('api/equipmentlist', 'EID'); - $id = $record['EID']; - $newName = 'Equipment ' . uniqid(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "api/equipmentlist/{$id}", ['InstrumentName' => $newName]); - - $patch->assertStatus(200, 'Equipment patch should return 200'); - $showData = $this->fetchResource('api/equipmentlist', $id); - - $this->assertEquals($newName, $showData['InstrumentName']); - $this->assertEquals($record['DepartmentID'] ?? null, $showData['DepartmentID'] ?? null); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createResource(string $endpoint, array $payload) + { + $response = $this->withHeaders($this->authHeaders()) + ->withBody(json_encode($payload)) + ->call('post', $endpoint); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + + $this->assertEquals('success', $decoded['status']); + + return $decoded['data']; + } + + private function fetchFirstRecord(string $endpoint, string $idKey): array + { + try { + $response = $this->withHeaders($this->authHeaders())->call('get', $endpoint); + } catch (PageNotFoundException $e) { + $this->markTestSkipped("{$endpoint} GET not available: {$e->getMessage()}"); + } + + $response->assertStatus(200); + $decoded = json_decode($response->getJSON(), true); + $rows = $decoded['data'] ?? []; + + if (empty($rows)) { + $this->markTestSkipped("No data available at {$endpoint}"); + } + + $record = $rows[0]; + $this->assertArrayHasKey($idKey, $record); + + return $record; + } + + private function fetchResource(string $endpoint, $id): array + { + try { + $response = $this->withHeaders($this->authHeaders()) + ->call('get', "$endpoint/{$id}"); + } catch (PageNotFoundException $e) { + $this->markTestSkipped("{$endpoint}/{$id} GET not available: {$e->getMessage()}"); + } + + $response->assertStatus(200); + $decoded = json_decode($response->getJSON(), true); + + return $decoded['data']; + } + + public function testPartialUpdateOccupation() + { + $occCode = 'PATCH_OCC_' . uniqid(); + $id = $this->createResource('api/occupation', [ + 'OccCode' => $occCode, + 'OccText' => 'Original text', + ]); + + $originalData = $this->fetchResource('api/occupation', $id); + $originalCode = $originalData['OccCode']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/occupation/{$id}", ['OccText' => 'Patched occupation']); + + $patch->assertStatus(201); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $showData = $this->fetchResource('api/occupation', $id); + + $this->assertEquals('Patched occupation', $showData['OccText']); + $this->assertEquals($originalCode, $showData['OccCode']); + } + + public function testPartialUpdateMedicalSpecialty() + { + $text = 'Specialty ' . uniqid(); + $id = $this->createResource('api/medicalspecialty', ['SpecialtyText' => $text]); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/medicalspecialty/{$id}", ['SpecialtyText' => 'Updated specialty']); + + $patch->assertStatus(201); + $show = $this->withHeaders($this->authHeaders())->call('get', "api/medicalspecialty/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated specialty', $showData['SpecialtyText']); + } + + public function testPartialUpdateCounter() + { + $initial = 'Counter ' . uniqid(); + $id = $this->createResource('api/counter', [ + 'CounterName' => $initial, + 'CounterValue' => 1, + 'CounterStart' => 1, + 'CounterEnd' => 10, + 'CounterReset' => 1, + ]); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/counter/{$id}", ['CounterName' => 'Updated counter']); + + $patch->assertStatus(201); + $show = $this->withHeaders($this->authHeaders())->call('get', "api/counter/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated counter', $showData['CounterName']); + $this->assertEquals(1, (int) $showData['CounterValue']); + } + + public function testPartialUpdateOrganizationAccount() + { + $name = 'Account ' . uniqid(); + $id = $this->createResource('api/organization/account', ['AccountName' => $name]); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/organization/account/{$id}", ['AccountName' => 'Updated account']); + + $patch->assertStatus(200); + $show = $this->withHeaders($this->authHeaders())->call('get', "api/organization/account/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated account', $showData['AccountName']); + } + + public function testPartialUpdateDiscipline() + { + $code = 'DIS_' . strtoupper(bin2hex(random_bytes(2))); + $name = 'Discipline ' . uniqid(); + $id = $this->createResource('api/organization/discipline', [ + 'DisciplineCode' => $code, + 'DisciplineName' => $name, + ]); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/organization/discipline/{$id}", ['DisciplineName' => 'Discipline Updated']); + + $patch->assertStatus(201); + $show = $this->withHeaders($this->authHeaders())->call('get', "api/organization/discipline/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Discipline Updated', $showData['DisciplineName']); + $this->assertEquals($code, $showData['DisciplineCode']); + } + + public function testPartialUpdateCodingSystem() + { + $abbr = 'CS' . strtoupper(bin2hex(random_bytes(2))); + $full = 'Full text ' . uniqid(); + $id = $this->createResource('api/organization/codingsys', [ + 'CodingSysAbb' => $abbr, + 'FullText' => $full, + ]); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/organization/codingsys/{$id}", ['FullText' => 'Updated full text']); + + $patch->assertStatus(201); + $show = $this->withHeaders($this->authHeaders())->call('get', "api/organization/codingsys/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated full text', $showData['FullText']); + $this->assertEquals($abbr, $showData['CodingSysAbb']); + } + + public function testPartialUpdateSpecimenContainer() + { + $record = $this->fetchFirstRecord('api/specimen/container', 'ConDefID'); + $id = $record['ConDefID']; + $newName = 'Patch Container ' . uniqid(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/specimen/container/{$id}", ['ConName' => $newName]); + + $patch->assertStatus(201); + $showData = $this->fetchResource('api/specimen/container', $id); + + $this->assertEquals($newName, $showData['ConName']); + $this->assertEquals($record['ConCode'] ?? null, $showData['ConCode'] ?? null); + } + + public function testPartialUpdateSpecimenPrep() + { + $record = $this->fetchFirstRecord('api/specimen/prep', 'SpcPrpID'); + $id = $record['SpcPrpID']; + $newDesc = 'Partial Prep ' . uniqid(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/specimen/prep/{$id}", ['Description' => $newDesc]); + + $patch->assertStatus(201); + $showData = $this->fetchResource('api/specimen/prep', $id); + + $this->assertEquals($newDesc, $showData['Description']); + $this->assertEquals($record['SpcStaID'] ?? null, $showData['SpcStaID'] ?? null); + } + + public function testPartialUpdateSpecimenStatus() + { + $record = $this->fetchFirstRecord('api/specimen/status', 'SpcStaID'); + $id = $record['SpcStaID']; + $newStatus = 'UpdatedStatus'; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/specimen/status/{$id}", ['SpcStatus' => $newStatus]); + + $patch->assertStatus(201); + $showData = $this->fetchResource('api/specimen/status', $id); + + $this->assertEquals($newStatus, $showData['SpcStatus']); + $this->assertEquals($record['OrderID'] ?? null, $showData['OrderID'] ?? null); + } + + public function testPartialUpdateSpecimenCollection() + { + $record = $this->fetchFirstRecord('api/specimen/collection', 'SpcColID'); + $id = $record['SpcColID']; + $newBodySite = 'BodySite ' . uniqid(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/specimen/collection/{$id}", ['BodySite' => $newBodySite]); + + $patch->assertStatus(201); + $showData = $this->fetchResource('api/specimen/collection', $id); + + $this->assertEquals($newBodySite, $showData['BodySite']); + $this->assertEquals($record['SpRole'] ?? null, $showData['SpRole'] ?? null); + } + + public function testPartialUpdateEquipmentList() + { + $record = $this->fetchFirstRecord('api/equipmentlist', 'EID'); + $id = $record['EID']; + $newName = 'Equipment ' . uniqid(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "api/equipmentlist/{$id}", ['InstrumentName' => $newName]); + + $patch->assertStatus(200, 'Equipment patch should return 200'); + $showData = $this->fetchResource('api/equipmentlist', $id); + + $this->assertEquals($newName, $showData['InstrumentName']); + $this->assertEquals($record['DepartmentID'] ?? null, $showData['DepartmentID'] ?? null); + } +} diff --git a/tests/feature/OrderTest/OrderTestPatchTest.php b/tests/feature/OrderTest/OrderTestPatchTest.php index e87076f..025c87d 100755 --- a/tests/feature/OrderTest/OrderTestPatchTest.php +++ b/tests/feature/OrderTest/OrderTestPatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createOrderTest(array $data = []): array - { - $payload = array_merge([ - 'OrderCode' => 'ORD_' . uniqid(), - 'OrderName' => 'Test Order ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateOrderTestSuccess() - { - $order = $this->createOrderTest(); - $id = $order['OrderID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['OrderName' => 'Updated Order']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Order', $showData['OrderName']); - $this->assertEquals($order['OrderCode'], $showData['OrderCode']); - } - - public function testPartialUpdateOrderTestNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['OrderName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateOrderTestInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['OrderName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateOrderTestEmptyPayload() - { - $order = $this->createOrderTest(); - $id = $order['OrderID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateOrderTestSingleField() - { - $order = $this->createOrderTest(); - $id = $order['OrderID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['OrderCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($order['OrderCode'], $showData['OrderCode']); - $this->assertEquals($order['OrderName'], $showData['OrderName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createOrderTest(array $data = []): array + { + $payload = array_merge([ + 'OrderCode' => 'ORD_' . uniqid(), + 'OrderName' => 'Test Order ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateOrderTestSuccess() + { + $order = $this->createOrderTest(); + $id = $order['OrderID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['OrderName' => 'Updated Order']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Order', $showData['OrderName']); + $this->assertEquals($order['OrderCode'], $showData['OrderCode']); + } + + public function testPartialUpdateOrderTestNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['OrderName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateOrderTestInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['OrderName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateOrderTestEmptyPayload() + { + $order = $this->createOrderTest(); + $id = $order['OrderID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateOrderTestSingleField() + { + $order = $this->createOrderTest(); + $id = $order['OrderID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['OrderCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($order['OrderCode'], $showData['OrderCode']); + $this->assertEquals($order['OrderName'], $showData['OrderName']); + } +} diff --git a/tests/feature/Organization/AccountPatchTest.php b/tests/feature/Organization/AccountPatchTest.php index 7c998c5..8abf178 100644 --- a/tests/feature/Organization/AccountPatchTest.php +++ b/tests/feature/Organization/AccountPatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createAccount(array $data = []): int - { - $payload = array_merge([ - 'AccountName' => 'Account ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - - return $decoded['data']; - } - - private function fetchAccount(int $id): array - { - $response = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $response->assertStatus(200); - - $decoded = json_decode($response->getJSON(), true); - - return $decoded['data'] ?? []; - } - - public function testPartialUpdateAccountSuccess() - { - $id = $this->createAccount(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['AccountName' => 'Updated Account']); - - $patch->assertStatus(200); - $this->assertSame('success', json_decode($patch->getJSON(), true)['status']); - - $account = $this->fetchAccount($id); - $this->assertEquals('Updated Account', $account['AccountName']); - $this->assertEquals($id, $account['AccountID']); - } - - public function testPartialUpdateAccountNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['AccountName' => 'Does not matter']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateAccountInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['AccountName' => 'Bad']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateAccountEmptyPayload() - { - $id = $this->createAccount(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateAccountSingleField() - { - $id = $this->createAccount(['AccountName' => 'Original Name']); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['AccountName' => 'New Name']); - - $patch->assertStatus(200); - $account = $this->fetchAccount($id); - - $this->assertEquals('New Name', $account['AccountName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createAccount(array $data = []): int + { + $payload = array_merge([ + 'AccountName' => 'Account ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + + return $decoded['data']; + } + + private function fetchAccount(int $id): array + { + $response = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $response->assertStatus(200); + + $decoded = json_decode($response->getJSON(), true); + + return $decoded['data'] ?? []; + } + + public function testPartialUpdateAccountSuccess() + { + $id = $this->createAccount(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['AccountName' => 'Updated Account']); + + $patch->assertStatus(200); + $this->assertSame('success', json_decode($patch->getJSON(), true)['status']); + + $account = $this->fetchAccount($id); + $this->assertEquals('Updated Account', $account['AccountName']); + $this->assertEquals($id, $account['AccountID']); + } + + public function testPartialUpdateAccountNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['AccountName' => 'Does not matter']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateAccountInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['AccountName' => 'Bad']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateAccountEmptyPayload() + { + $id = $this->createAccount(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateAccountSingleField() + { + $id = $this->createAccount(['AccountName' => 'Original Name']); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['AccountName' => 'New Name']); + + $patch->assertStatus(200); + $account = $this->fetchAccount($id); + + $this->assertEquals('New Name', $account['AccountName']); + } +} diff --git a/tests/feature/Organization/CodingSysControllerTest.php b/tests/feature/Organization/CodingSysControllerTest.php index c1faa1f..e610684 100755 --- a/tests/feature/Organization/CodingSysControllerTest.php +++ b/tests/feature/Organization/CodingSysControllerTest.php @@ -1,31 +1,31 @@ -get($this->endpoint); - $result->assertStatus(200); - } - - public function testCreateCodingSys() - { - $payload = [ - 'CodingSysAbb' => 'ICD' . substr(time(), -3), - 'FullText' => 'International Classification of Diseases 10 ' . time(), - 'Description' => 'Medical diagnosis coding system' - ]; - - $result = $this->withBodyFormat('json')->post($this->endpoint, $payload); - $result->assertStatus(201); - } -} +get($this->endpoint); + $result->assertStatus(200); + } + + public function testCreateCodingSys() + { + $payload = [ + 'CodingSysAbb' => 'ICD' . substr(time(), -3), + 'FullText' => 'International Classification of Diseases 10 ' . time(), + 'Description' => 'Medical diagnosis coding system' + ]; + + $result = $this->withBodyFormat('json')->post($this->endpoint, $payload); + $result->assertStatus(201); + } +} diff --git a/tests/feature/Organization/DepartmentPatchTest.php b/tests/feature/Organization/DepartmentPatchTest.php index 9f792ba..18e6364 100755 --- a/tests/feature/Organization/DepartmentPatchTest.php +++ b/tests/feature/Organization/DepartmentPatchTest.php @@ -1,123 +1,123 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createDepartment(array $data = []): array - { - $payload = array_merge([ - 'DepartmentCode' => 'DEPT_' . uniqid(), - 'DepartmentName' => 'Test Department ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - if ($response->getStatusCode() !== 201) { - $this->markTestSkipped('Failed to create test department'); - } - $decoded = json_decode($response->getJSON(), true); - return $decoded['data'] ?? []; - } - - public function testPartialUpdateDepartmentSuccess() - { - $dept = $this->createDepartment(); - $id = $dept['DepartmentID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['DepartmentName' => 'Updated Department']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Department', $showData['DepartmentName']); - $this->assertEquals($dept['DepartmentCode'], $showData['DepartmentCode']); - } - - public function testPartialUpdateDepartmentNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['DepartmentName' => 'Updated']); - - $this->assertTrue(in_array($patch->getStatusCode(), [404, 400, 201])); - } - - public function testPartialUpdateDepartmentZeroId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/0", ['DepartmentName' => 'Updated']); - - $this->assertTrue(in_array($patch->getStatusCode(), [404, 400, 201])); - } - - public function testPartialUpdateDepartmentEmptyPayload() - { - $dept = $this->createDepartment(); - $id = $dept['DepartmentID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateDepartmentSingleField() - { - $dept = $this->createDepartment(); - $id = $dept['DepartmentID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['DepartmentCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($dept['DepartmentCode'], $showData['DepartmentCode']); - $this->assertEquals($dept['DepartmentName'], $showData['DepartmentName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createDepartment(array $data = []): array + { + $payload = array_merge([ + 'DepartmentCode' => 'DEPT_' . uniqid(), + 'DepartmentName' => 'Test Department ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + if ($response->getStatusCode() !== 201) { + $this->markTestSkipped('Failed to create test department'); + } + $decoded = json_decode($response->getJSON(), true); + return $decoded['data'] ?? []; + } + + public function testPartialUpdateDepartmentSuccess() + { + $dept = $this->createDepartment(); + $id = $dept['DepartmentID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['DepartmentName' => 'Updated Department']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Department', $showData['DepartmentName']); + $this->assertEquals($dept['DepartmentCode'], $showData['DepartmentCode']); + } + + public function testPartialUpdateDepartmentNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['DepartmentName' => 'Updated']); + + $this->assertTrue(in_array($patch->getStatusCode(), [404, 400, 201])); + } + + public function testPartialUpdateDepartmentZeroId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/0", ['DepartmentName' => 'Updated']); + + $this->assertTrue(in_array($patch->getStatusCode(), [404, 400, 201])); + } + + public function testPartialUpdateDepartmentEmptyPayload() + { + $dept = $this->createDepartment(); + $id = $dept['DepartmentID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateDepartmentSingleField() + { + $dept = $this->createDepartment(); + $id = $dept['DepartmentID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['DepartmentCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($dept['DepartmentCode'], $showData['DepartmentCode']); + $this->assertEquals($dept['DepartmentName'], $showData['DepartmentName']); + } +} diff --git a/tests/feature/Organization/DisciplinePatchTest.php b/tests/feature/Organization/DisciplinePatchTest.php index 9ffdfd0..421591f 100644 --- a/tests/feature/Organization/DisciplinePatchTest.php +++ b/tests/feature/Organization/DisciplinePatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createDiscipline(array $data = []): int - { - $payload = array_merge([ - 'DisciplineCode' => 'D' . strtoupper(bin2hex(random_bytes(1))), - 'DisciplineName' => 'Discipline ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - - return $decoded['data']; - } - - private function fetchDiscipline(int $id): array - { - $response = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $response->assertStatus(200); - $decoded = json_decode($response->getJSON(), true); - - return $decoded['data'] ?? []; - } - - public function testPartialUpdateDisciplineSuccess() - { - $id = $this->createDiscipline(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['DisciplineName' => 'Updated Discipline']); - - $patch->assertStatus(200); - $this->assertSame('success', json_decode($patch->getJSON(), true)['status']); - - $discipline = $this->fetchDiscipline($id); - $this->assertEquals('Updated Discipline', $discipline['DisciplineName']); - $this->assertEquals($id, $discipline['DisciplineID']); - } - - public function testPartialUpdateDisciplineNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['DisciplineName' => 'Does not matter']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateDisciplineInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['DisciplineName' => 'Bad']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateDisciplineEmptyPayload() - { - $id = $this->createDiscipline(); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateDisciplineSingleField() - { - $id = $this->createDiscipline(['DisciplineName' => 'Original Name']); - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['DisciplineName' => 'New Name']); - - $patch->assertStatus(200); - $discipline = $this->fetchDiscipline($id); - - $this->assertEquals('New Name', $discipline['DisciplineName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createDiscipline(array $data = []): int + { + $payload = array_merge([ + 'DisciplineCode' => 'D' . strtoupper(bin2hex(random_bytes(1))), + 'DisciplineName' => 'Discipline ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + + return $decoded['data']; + } + + private function fetchDiscipline(int $id): array + { + $response = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $response->assertStatus(200); + $decoded = json_decode($response->getJSON(), true); + + return $decoded['data'] ?? []; + } + + public function testPartialUpdateDisciplineSuccess() + { + $id = $this->createDiscipline(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['DisciplineName' => 'Updated Discipline']); + + $patch->assertStatus(200); + $this->assertSame('success', json_decode($patch->getJSON(), true)['status']); + + $discipline = $this->fetchDiscipline($id); + $this->assertEquals('Updated Discipline', $discipline['DisciplineName']); + $this->assertEquals($id, $discipline['DisciplineID']); + } + + public function testPartialUpdateDisciplineNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['DisciplineName' => 'Does not matter']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateDisciplineInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['DisciplineName' => 'Bad']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateDisciplineEmptyPayload() + { + $id = $this->createDiscipline(); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateDisciplineSingleField() + { + $id = $this->createDiscipline(['DisciplineName' => 'Original Name']); + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['DisciplineName' => 'New Name']); + + $patch->assertStatus(200); + $discipline = $this->fetchDiscipline($id); + + $this->assertEquals('New Name', $discipline['DisciplineName']); + } +} diff --git a/tests/feature/Organization/HostAppControllerTest.php b/tests/feature/Organization/HostAppControllerTest.php index 9a5c00b..2b45306 100755 --- a/tests/feature/Organization/HostAppControllerTest.php +++ b/tests/feature/Organization/HostAppControllerTest.php @@ -1,35 +1,35 @@ -get($this->endpoint); - $result->assertStatus(200); - } - - public function testCreateHostApp() - { - $payload = [ - 'HostAppName' => 'Test Host Application', - 'SiteID' => null - ]; - - $result = $this->withBodyFormat('json')->post($this->endpoint, $payload); - $result->assertStatus(201); - } -} +get($this->endpoint); + $result->assertStatus(200); + } + + public function testCreateHostApp() + { + $payload = [ + 'HostAppName' => 'Test Host Application', + 'SiteID' => null + ]; + + $result = $this->withBodyFormat('json')->post($this->endpoint, $payload); + $result->assertStatus(201); + } +} diff --git a/tests/feature/Organization/HostAppPatchTest.php b/tests/feature/Organization/HostAppPatchTest.php index b813364..4220be6 100755 --- a/tests/feature/Organization/HostAppPatchTest.php +++ b/tests/feature/Organization/HostAppPatchTest.php @@ -1,122 +1,122 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createHostApp(array $data = []): array - { - $payload = array_merge([ - 'HostAppName' => 'Test HostApp ' . uniqid(), - 'SiteID' => null, - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - $id = $decoded['data']; - return array_merge(['HostAppID' => $id], $payload); - } - - public function testPartialUpdateHostAppSuccess() - { - $app = $this->createHostApp(); - $id = $app['HostAppID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['HostAppName' => 'Updated HostApp']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated HostApp', $showData['HostAppName']); - $this->assertEquals($app['SiteID'], $showData['SiteID']); - } - - public function testPartialUpdateHostAppNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['HostAppName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateHostAppInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['HostAppName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateHostAppEmptyPayload() - { - $app = $this->createHostApp(); - $id = $app['HostAppID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateHostAppSingleField() - { - $app = $this->createHostApp(); - $id = $app['HostAppID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SiteID' => 5]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($app['SiteID'], $showData['SiteID']); - $this->assertEquals($app['HostAppName'], $showData['HostAppName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createHostApp(array $data = []): array + { + $payload = array_merge([ + 'HostAppName' => 'Test HostApp ' . uniqid(), + 'SiteID' => null, + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + $id = $decoded['data']; + return array_merge(['HostAppID' => $id], $payload); + } + + public function testPartialUpdateHostAppSuccess() + { + $app = $this->createHostApp(); + $id = $app['HostAppID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['HostAppName' => 'Updated HostApp']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated HostApp', $showData['HostAppName']); + $this->assertEquals($app['SiteID'], $showData['SiteID']); + } + + public function testPartialUpdateHostAppNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['HostAppName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateHostAppInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['HostAppName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateHostAppEmptyPayload() + { + $app = $this->createHostApp(); + $id = $app['HostAppID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateHostAppSingleField() + { + $app = $this->createHostApp(); + $id = $app['HostAppID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SiteID' => 5]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($app['SiteID'], $showData['SiteID']); + $this->assertEquals($app['HostAppName'], $showData['HostAppName']); + } +} diff --git a/tests/feature/Organization/HostComParaPatchTest.php b/tests/feature/Organization/HostComParaPatchTest.php index c46b2f7..d098671 100755 --- a/tests/feature/Organization/HostComParaPatchTest.php +++ b/tests/feature/Organization/HostComParaPatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createHostComPara(array $data = []): array - { - $payload = array_merge([ - 'HostComParaCode' => 'HCP_' . uniqid(), - 'HostComParaName' => 'Test HostComPara ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateHostComParaSuccess() - { - $para = $this->createHostComPara(); - $id = $para['HostComParaID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['HostComParaName' => 'Updated HostComPara']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated HostComPara', $showData['HostComParaName']); - $this->assertEquals($para['HostComParaCode'], $showData['HostComParaCode']); - } - - public function testPartialUpdateHostComParaNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['HostComParaName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateHostComParaInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['HostComParaName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateHostComParaEmptyPayload() - { - $para = $this->createHostComPara(); - $id = $para['HostComParaID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateHostComParaSingleField() - { - $para = $this->createHostComPara(); - $id = $para['HostComParaID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['HostComParaCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($para['HostComParaCode'], $showData['HostComParaCode']); - $this->assertEquals($para['HostComParaName'], $showData['HostComParaName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createHostComPara(array $data = []): array + { + $payload = array_merge([ + 'HostComParaCode' => 'HCP_' . uniqid(), + 'HostComParaName' => 'Test HostComPara ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateHostComParaSuccess() + { + $para = $this->createHostComPara(); + $id = $para['HostComParaID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['HostComParaName' => 'Updated HostComPara']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated HostComPara', $showData['HostComParaName']); + $this->assertEquals($para['HostComParaCode'], $showData['HostComParaCode']); + } + + public function testPartialUpdateHostComParaNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['HostComParaName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateHostComParaInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['HostComParaName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateHostComParaEmptyPayload() + { + $para = $this->createHostComPara(); + $id = $para['HostComParaID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateHostComParaSingleField() + { + $para = $this->createHostComPara(); + $id = $para['HostComParaID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['HostComParaCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($para['HostComParaCode'], $showData['HostComParaCode']); + $this->assertEquals($para['HostComParaName'], $showData['HostComParaName']); + } +} diff --git a/tests/feature/Organization/SitePatchTest.php b/tests/feature/Organization/SitePatchTest.php index e9fe5e8..75be9b3 100755 --- a/tests/feature/Organization/SitePatchTest.php +++ b/tests/feature/Organization/SitePatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createSite(array $data = []): array - { - $payload = array_merge([ - 'SiteCode' => 'SITE_' . uniqid(), - 'SiteName' => 'Test Site ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateSiteSuccess() - { - $site = $this->createSite(); - $id = $site['SiteID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SiteName' => 'Updated Site']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Site', $showData['SiteName']); - $this->assertEquals($site['SiteCode'], $showData['SiteCode']); - } - - public function testPartialUpdateSiteNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['SiteName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateSiteInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['SiteName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateSiteEmptyPayload() - { - $site = $this->createSite(); - $id = $site['SiteID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateSiteSingleField() - { - $site = $this->createSite(); - $id = $site['SiteID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SiteCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($site['SiteCode'], $showData['SiteCode']); - $this->assertEquals($site['SiteName'], $showData['SiteName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createSite(array $data = []): array + { + $payload = array_merge([ + 'SiteCode' => 'SITE_' . uniqid(), + 'SiteName' => 'Test Site ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateSiteSuccess() + { + $site = $this->createSite(); + $id = $site['SiteID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SiteName' => 'Updated Site']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Site', $showData['SiteName']); + $this->assertEquals($site['SiteCode'], $showData['SiteCode']); + } + + public function testPartialUpdateSiteNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['SiteName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateSiteInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['SiteName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateSiteEmptyPayload() + { + $site = $this->createSite(); + $id = $site['SiteID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateSiteSingleField() + { + $site = $this->createSite(); + $id = $site['SiteID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SiteCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($site['SiteCode'], $showData['SiteCode']); + $this->assertEquals($site['SiteName'], $showData['SiteName']); + } +} diff --git a/tests/feature/Organization/WorkstationPatchTest.php b/tests/feature/Organization/WorkstationPatchTest.php index 81ba6dc..7276984 100755 --- a/tests/feature/Organization/WorkstationPatchTest.php +++ b/tests/feature/Organization/WorkstationPatchTest.php @@ -1,122 +1,122 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createWorkstation(array $data = []): array - { - $payload = array_merge([ - 'WorkstationCode' => 'WS_' . uniqid(), - 'WorkstationName' => 'Test Workstation ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - $id = $decoded['data']; - return array_merge(['WorkstationID' => $id], $payload); - } - - public function testPartialUpdateWorkstationSuccess() - { - $ws = $this->createWorkstation(); - $id = $ws['WorkstationID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['WorkstationName' => 'Updated Workstation']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Workstation', $showData['WorkstationName']); - $this->assertEquals($ws['WorkstationCode'], $showData['WorkstationCode']); - } - - public function testPartialUpdateWorkstationNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['WorkstationName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateWorkstationInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['WorkstationName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateWorkstationEmptyPayload() - { - $ws = $this->createWorkstation(); - $id = $ws['WorkstationID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateWorkstationSingleField() - { - $ws = $this->createWorkstation(); - $id = $ws['WorkstationID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['WorkstationCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($ws['WorkstationCode'], $showData['WorkstationCode']); - $this->assertEquals($ws['WorkstationName'], $showData['WorkstationName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createWorkstation(array $data = []): array + { + $payload = array_merge([ + 'WorkstationCode' => 'WS_' . uniqid(), + 'WorkstationName' => 'Test Workstation ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + $id = $decoded['data']; + return array_merge(['WorkstationID' => $id], $payload); + } + + public function testPartialUpdateWorkstationSuccess() + { + $ws = $this->createWorkstation(); + $id = $ws['WorkstationID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['WorkstationName' => 'Updated Workstation']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Workstation', $showData['WorkstationName']); + $this->assertEquals($ws['WorkstationCode'], $showData['WorkstationCode']); + } + + public function testPartialUpdateWorkstationNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['WorkstationName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateWorkstationInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['WorkstationName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateWorkstationEmptyPayload() + { + $ws = $this->createWorkstation(); + $id = $ws['WorkstationID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateWorkstationSingleField() + { + $ws = $this->createWorkstation(); + $id = $ws['WorkstationID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['WorkstationCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($ws['WorkstationCode'], $showData['WorkstationCode']); + $this->assertEquals($ws['WorkstationName'], $showData['WorkstationName']); + } +} diff --git a/tests/feature/PatVisit/PatVisitADTPatchTest.php b/tests/feature/PatVisit/PatVisitADTPatchTest.php index 69e3296..30ddd6d 100755 --- a/tests/feature/PatVisit/PatVisitADTPatchTest.php +++ b/tests/feature/PatVisit/PatVisitADTPatchTest.php @@ -1,97 +1,97 @@ - $this->createTestPatient(), - 'ADTCode' => 'A01', - 'LocationID' => '1', - ], $data); - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdatePatVisitADTSuccess() - { - $adt = $this->createPatVisitADT(); - $id = $adt['ADTID']; - - $patch = $this->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ADTCode' => 'A02']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('A02', $showData['ADTCode']); - $this->assertEquals($adt['LocationID'], $showData['LocationID']); - } - - public function testPartialUpdatePatVisitADTNotFound() - { - $patch = $this->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['ADTCode' => 'A02']); - - $patch->assertStatus(404); - } - - public function testPartialUpdatePatVisitADTInvalidId() - { - $patch = $this->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['ADTCode' => 'A02']); - - $patch->assertStatus(400); - } - - public function testPartialUpdatePatVisitADTEmptyPayload() - { - $adt = $this->createPatVisitADT(); - $id = $adt['ADTID']; - - $patch = $this->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdatePatVisitADTSingleField() - { - $adt = $this->createPatVisitADT(); - $id = $adt['ADTID']; - - $patch = $this->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ADTCode' => 'A03']); - - $patch->assertStatus(200); - $showData = json_decode($this->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertEquals('A03', $showData['ADTCode']); - $this->assertEquals($adt['LocationID'], $showData['LocationID']); - } -} + $this->createTestPatient(), + 'ADTCode' => 'A01', + 'LocationID' => '1', + ], $data); + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdatePatVisitADTSuccess() + { + $adt = $this->createPatVisitADT(); + $id = $adt['ADTID']; + + $patch = $this->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ADTCode' => 'A02']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('A02', $showData['ADTCode']); + $this->assertEquals($adt['LocationID'], $showData['LocationID']); + } + + public function testPartialUpdatePatVisitADTNotFound() + { + $patch = $this->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['ADTCode' => 'A02']); + + $patch->assertStatus(404); + } + + public function testPartialUpdatePatVisitADTInvalidId() + { + $patch = $this->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['ADTCode' => 'A02']); + + $patch->assertStatus(400); + } + + public function testPartialUpdatePatVisitADTEmptyPayload() + { + $adt = $this->createPatVisitADT(); + $id = $adt['ADTID']; + + $patch = $this->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdatePatVisitADTSingleField() + { + $adt = $this->createPatVisitADT(); + $id = $adt['ADTID']; + + $patch = $this->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ADTCode' => 'A03']); + + $patch->assertStatus(200); + $showData = json_decode($this->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertEquals('A03', $showData['ADTCode']); + $this->assertEquals($adt['LocationID'], $showData['LocationID']); + } +} diff --git a/tests/feature/Patients/PatientCheckTest.php b/tests/feature/Patients/PatientCheckTest.php index 0ef1194..5f54238 100755 --- a/tests/feature/Patients/PatientCheckTest.php +++ b/tests/feature/Patients/PatientCheckTest.php @@ -1,119 +1,119 @@ -withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'PatientID' => 'SMAJ1', - ]); - - $result->assertStatus(200); - $data = json_decode($result->getJSON(), true); - - $this->assertSame('success', $data['status']); - $this->assertSame(false, $data['data']); - } - - public function testCheckPatientIDWithHyphenExists() - { - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'PatientID' => 'SMAJ-1', - ]); - - $result->assertStatus(200); - $data = json_decode($result->getJSON(), true); - - $this->assertSame('success', $data['status']); - $this->assertIsBool($data['data']); - } - - public function testCheckPatientIDNotExists() - { - $faker = Factory::create(); - - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'PatientID' => 'NONEXISTENT-' . $faker->numberBetween(100000, 999999), - ]); - - $result->assertStatus(200); - $data = json_decode($result->getJSON(), true); - - $this->assertSame('success', $data['status']); - $this->assertTrue($data['data']); - } - - public function testCheckEmailAddressExists() - { - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'EmailAddress' => 'dummy1@test.com', - ]); - - $result->assertStatus(200); - $data = json_decode($result->getJSON(), true); - - $this->assertSame('success', $data['status']); - $this->assertIsBool($data['data']); - } - - public function testCheckEmailAddressMatchesSecondaryColumn() - { - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'EmailAddress' => 'dummy1@test.com', - ]); - - $result->assertStatus(200); - $data = json_decode($result->getJSON(), true); - - $this->assertSame('success', $data['status']); - $this->assertIsBool($data['data']); - } - - public function testCheckPhoneExists() - { - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'Phone' => '092029', - ]); - - $result->assertStatus(200); - $data = json_decode($result->getJSON(), true); - - $this->assertSame('success', $data['status']); - $this->assertIsBool($data['data']); - } - - public function testCheckWithoutParams() - { - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint); - - $result->assertStatus(400); - $data = json_decode($result->getJSON(), true); - - $this->assertSame('error', $data['status']); - $this->assertSame('PatientID, EmailAddress, or Phone parameter is required.', $data['message']); - } - - public function testCheckResponseStructure() - { - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'PatientID' => 'TEST123', - ]); - - $result->assertStatus(200); - $data = json_decode($result->getJSON(), true); - - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('message', $data); - $this->assertArrayHasKey('data', $data); - } -} +withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'PatientID' => 'SMAJ1', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertSame(false, $data['data']); + } + + public function testCheckPatientIDWithHyphenExists() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'PatientID' => 'SMAJ-1', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckPatientIDNotExists() + { + $faker = Factory::create(); + + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'PatientID' => 'NONEXISTENT-' . $faker->numberBetween(100000, 999999), + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertTrue($data['data']); + } + + public function testCheckEmailAddressExists() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'EmailAddress' => 'dummy1@test.com', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckEmailAddressMatchesSecondaryColumn() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'EmailAddress' => 'dummy1@test.com', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckPhoneExists() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'Phone' => '092029', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckWithoutParams() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint); + + $result->assertStatus(400); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('error', $data['status']); + $this->assertSame('PatientID, EmailAddress, or Phone parameter is required.', $data['message']); + } + + public function testCheckResponseStructure() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'PatientID' => 'TEST123', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('message', $data); + $this->assertArrayHasKey('data', $data); + } +} diff --git a/tests/feature/Result/ResultPatchTest.php b/tests/feature/Result/ResultPatchTest.php index 577fda6..fb362ea 100755 --- a/tests/feature/Result/ResultPatchTest.php +++ b/tests/feature/Result/ResultPatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createResult(array $data = []): array - { - $payload = array_merge([ - 'ResultCode' => 'RES_' . uniqid(), - 'ResultValue' => 'Test Value ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateResultSuccess() - { - $result = $this->createResult(); - $id = $result['ResultID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ResultValue' => 'Updated Value']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Value', $showData['ResultValue']); - $this->assertEquals($result['ResultCode'], $showData['ResultCode']); - } - - public function testPartialUpdateResultNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['ResultValue' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateResultInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['ResultValue' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateResultEmptyPayload() - { - $result = $this->createResult(); - $id = $result['ResultID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateResultSingleField() - { - $result = $this->createResult(); - $id = $result['ResultID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ResultCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($result['ResultCode'], $showData['ResultCode']); - $this->assertEquals($result['ResultValue'], $showData['ResultValue']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createResult(array $data = []): array + { + $payload = array_merge([ + 'ResultCode' => 'RES_' . uniqid(), + 'ResultValue' => 'Test Value ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateResultSuccess() + { + $result = $this->createResult(); + $id = $result['ResultID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ResultValue' => 'Updated Value']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Value', $showData['ResultValue']); + $this->assertEquals($result['ResultCode'], $showData['ResultCode']); + } + + public function testPartialUpdateResultNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['ResultValue' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateResultInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['ResultValue' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateResultEmptyPayload() + { + $result = $this->createResult(); + $id = $result['ResultID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateResultSingleField() + { + $result = $this->createResult(); + $id = $result['ResultID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ResultCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($result['ResultCode'], $showData['ResultCode']); + $this->assertEquals($result['ResultValue'], $showData['ResultValue']); + } +} diff --git a/tests/feature/Rule/RulePatchTest.php b/tests/feature/Rule/RulePatchTest.php index 7a581e7..7bc0501 100755 --- a/tests/feature/Rule/RulePatchTest.php +++ b/tests/feature/Rule/RulePatchTest.php @@ -1,122 +1,122 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createRule(array $data = []): array - { - $payload = array_merge([ - 'RuleCode' => 'RULE_' . uniqid(), - 'RuleName' => 'Test Rule ' . uniqid(), - 'RuleExpression' => 'test_expression', - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateRuleSuccess() - { - $rule = $this->createRule(); - $id = $rule['RuleID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['RuleName' => 'Updated Rule']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Rule', $showData['RuleName']); - $this->assertEquals($rule['RuleCode'], $showData['RuleCode']); - } - - public function testPartialUpdateRuleNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['RuleName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateRuleInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['RuleName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateRuleEmptyPayload() - { - $rule = $this->createRule(); - $id = $rule['RuleID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateRuleSingleField() - { - $rule = $this->createRule(); - $id = $rule['RuleID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['RuleCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($rule['RuleCode'], $showData['RuleCode']); - $this->assertEquals($rule['RuleName'], $showData['RuleName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createRule(array $data = []): array + { + $payload = array_merge([ + 'RuleCode' => 'RULE_' . uniqid(), + 'RuleName' => 'Test Rule ' . uniqid(), + 'RuleExpression' => 'test_expression', + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateRuleSuccess() + { + $rule = $this->createRule(); + $id = $rule['RuleID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['RuleName' => 'Updated Rule']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Rule', $showData['RuleName']); + $this->assertEquals($rule['RuleCode'], $showData['RuleCode']); + } + + public function testPartialUpdateRuleNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['RuleName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateRuleInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['RuleName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateRuleEmptyPayload() + { + $rule = $this->createRule(); + $id = $rule['RuleID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateRuleSingleField() + { + $rule = $this->createRule(); + $id = $rule['RuleID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['RuleCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($rule['RuleCode'], $showData['RuleCode']); + $this->assertEquals($rule['RuleName'], $showData['RuleName']); + } +} diff --git a/tests/feature/Specimen/CollectionPatchTest.php b/tests/feature/Specimen/CollectionPatchTest.php index 414d250..e7f9f70 100755 --- a/tests/feature/Specimen/CollectionPatchTest.php +++ b/tests/feature/Specimen/CollectionPatchTest.php @@ -1,118 +1,118 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createCollection(array $data = []): array - { - $payload = array_merge([ - 'BodySite' => 'BodySite_' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateCollectionSuccess() - { - $collection = $this->createCollection(); - $id = $collection['SpcColID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['BodySite' => 'UpdatedBodySite']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('UpdatedBodySite', $showData['BodySite']); - } - - public function testPartialUpdateCollectionNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['BodySite' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateCollectionInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['BodySite' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateCollectionEmptyPayload() - { - $collection = $this->createCollection(); - $id = $collection['SpcColID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateCollectionSingleField() - { - $collection = $this->createCollection(); - $id = $collection['SpcColID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['BodySite' => 'NewBodySite']); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertEquals('NewBodySite', $showData['BodySite']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createCollection(array $data = []): array + { + $payload = array_merge([ + 'BodySite' => 'BodySite_' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateCollectionSuccess() + { + $collection = $this->createCollection(); + $id = $collection['SpcColID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['BodySite' => 'UpdatedBodySite']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('UpdatedBodySite', $showData['BodySite']); + } + + public function testPartialUpdateCollectionNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['BodySite' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateCollectionInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['BodySite' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateCollectionEmptyPayload() + { + $collection = $this->createCollection(); + $id = $collection['SpcColID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateCollectionSingleField() + { + $collection = $this->createCollection(); + $id = $collection['SpcColID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['BodySite' => 'NewBodySite']); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertEquals('NewBodySite', $showData['BodySite']); + } +} diff --git a/tests/feature/Specimen/ContainerPatchTest.php b/tests/feature/Specimen/ContainerPatchTest.php index 2a8b65c..76de4b8 100755 --- a/tests/feature/Specimen/ContainerPatchTest.php +++ b/tests/feature/Specimen/ContainerPatchTest.php @@ -1,122 +1,122 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createContainer(array $data = []): array - { - $payload = array_merge([ - 'ConCode' => 'CON_' . uniqid(), - 'ConName' => 'Test Container ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - $id = $decoded['data']; - return array_merge(['ConDefID' => $id], $payload); - } - - public function testPartialUpdateContainerSuccess() - { - $container = $this->createContainer(); - $id = $container['ConDefID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ConName' => 'Updated Container']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Container', $showData['ConName']); - $this->assertEquals($container['ConCode'], $showData['ConCode']); - } - - public function testPartialUpdateContainerNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['ConName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateContainerInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['ConName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateContainerEmptyPayload() - { - $container = $this->createContainer(); - $id = $container['ConDefID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateContainerSingleField() - { - $container = $this->createContainer(); - $id = $container['ConDefID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ConCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($container['ConCode'], $showData['ConCode']); - $this->assertEquals($container['ConName'], $showData['ConName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createContainer(array $data = []): array + { + $payload = array_merge([ + 'ConCode' => 'CON_' . uniqid(), + 'ConName' => 'Test Container ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + $id = $decoded['data']; + return array_merge(['ConDefID' => $id], $payload); + } + + public function testPartialUpdateContainerSuccess() + { + $container = $this->createContainer(); + $id = $container['ConDefID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ConName' => 'Updated Container']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Container', $showData['ConName']); + $this->assertEquals($container['ConCode'], $showData['ConCode']); + } + + public function testPartialUpdateContainerNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['ConName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateContainerInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['ConName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateContainerEmptyPayload() + { + $container = $this->createContainer(); + $id = $container['ConDefID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateContainerSingleField() + { + $container = $this->createContainer(); + $id = $container['ConDefID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ConCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($container['ConCode'], $showData['ConCode']); + $this->assertEquals($container['ConName'], $showData['ConName']); + } +} diff --git a/tests/feature/Specimen/PrepPatchTest.php b/tests/feature/Specimen/PrepPatchTest.php index 3b32cdf..7d2ac48 100755 --- a/tests/feature/Specimen/PrepPatchTest.php +++ b/tests/feature/Specimen/PrepPatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createPrep(array $data = []): array - { - $payload = array_merge([ - 'SpcPrpCode' => 'PREP_' . uniqid(), - 'Description' => 'Test Prep ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdatePrepSuccess() - { - $prep = $this->createPrep(); - $id = $prep['SpcPrpID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['Description' => 'Updated Prep']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Prep', $showData['Description']); - $this->assertEquals($prep['SpcPrpCode'], $showData['SpcPrpCode']); - } - - public function testPartialUpdatePrepNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['Description' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdatePrepInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['Description' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdatePrepEmptyPayload() - { - $prep = $this->createPrep(); - $id = $prep['SpcPrpID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdatePrepSingleField() - { - $prep = $this->createPrep(); - $id = $prep['SpcPrpID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SpcPrpCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($prep['SpcPrpCode'], $showData['SpcPrpCode']); - $this->assertEquals($prep['Description'], $showData['Description']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createPrep(array $data = []): array + { + $payload = array_merge([ + 'SpcPrpCode' => 'PREP_' . uniqid(), + 'Description' => 'Test Prep ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdatePrepSuccess() + { + $prep = $this->createPrep(); + $id = $prep['SpcPrpID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['Description' => 'Updated Prep']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Prep', $showData['Description']); + $this->assertEquals($prep['SpcPrpCode'], $showData['SpcPrpCode']); + } + + public function testPartialUpdatePrepNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['Description' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdatePrepInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['Description' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdatePrepEmptyPayload() + { + $prep = $this->createPrep(); + $id = $prep['SpcPrpID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdatePrepSingleField() + { + $prep = $this->createPrep(); + $id = $prep['SpcPrpID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SpcPrpCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($prep['SpcPrpCode'], $showData['SpcPrpCode']); + $this->assertEquals($prep['Description'], $showData['Description']); + } +} diff --git a/tests/feature/Specimen/SpecimenPatchTest.php b/tests/feature/Specimen/SpecimenPatchTest.php index bcf12da..fcc66a0 100755 --- a/tests/feature/Specimen/SpecimenPatchTest.php +++ b/tests/feature/Specimen/SpecimenPatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createSpecimen(array $data = []): array - { - $payload = array_merge([ - 'SpecimenCode' => 'SPEC_' . uniqid(), - 'SpecimenName' => 'Test Specimen ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateSpecimenSuccess() - { - $specimen = $this->createSpecimen(); - $id = $specimen['SpecimenID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SpecimenName' => 'Updated Specimen']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Specimen', $showData['SpecimenName']); - $this->assertEquals($specimen['SpecimenCode'], $showData['SpecimenCode']); - } - - public function testPartialUpdateSpecimenNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['SpecimenName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateSpecimenInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['SpecimenName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateSpecimenEmptyPayload() - { - $specimen = $this->createSpecimen(); - $id = $specimen['SpecimenID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateSpecimenSingleField() - { - $specimen = $this->createSpecimen(); - $id = $specimen['SpecimenID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SpecimenCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($specimen['SpecimenCode'], $showData['SpecimenCode']); - $this->assertEquals($specimen['SpecimenName'], $showData['SpecimenName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createSpecimen(array $data = []): array + { + $payload = array_merge([ + 'SpecimenCode' => 'SPEC_' . uniqid(), + 'SpecimenName' => 'Test Specimen ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateSpecimenSuccess() + { + $specimen = $this->createSpecimen(); + $id = $specimen['SpecimenID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SpecimenName' => 'Updated Specimen']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Specimen', $showData['SpecimenName']); + $this->assertEquals($specimen['SpecimenCode'], $showData['SpecimenCode']); + } + + public function testPartialUpdateSpecimenNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['SpecimenName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateSpecimenInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['SpecimenName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateSpecimenEmptyPayload() + { + $specimen = $this->createSpecimen(); + $id = $specimen['SpecimenID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateSpecimenSingleField() + { + $specimen = $this->createSpecimen(); + $id = $specimen['SpecimenID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SpecimenCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($specimen['SpecimenCode'], $showData['SpecimenCode']); + $this->assertEquals($specimen['SpecimenName'], $showData['SpecimenName']); + } +} diff --git a/tests/feature/Specimen/StatusPatchTest.php b/tests/feature/Specimen/StatusPatchTest.php index e8bfe4b..25fee06 100755 --- a/tests/feature/Specimen/StatusPatchTest.php +++ b/tests/feature/Specimen/StatusPatchTest.php @@ -1,118 +1,118 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createStatus(array $data = []): array - { - $payload = array_merge([ - 'SpcStatus' => 'Status_' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateStatusSuccess() - { - $status = $this->createStatus(); - $id = $status['SpcStaID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SpcStatus' => 'UpdatedStatus']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('UpdatedStatus', $showData['SpcStatus']); - } - - public function testPartialUpdateStatusNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['SpcStatus' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateStatusInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['SpcStatus' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateStatusEmptyPayload() - { - $status = $this->createStatus(); - $id = $status['SpcStaID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateStatusSingleField() - { - $status = $this->createStatus(); - $id = $status['SpcStaID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['SpcStatus' => 'NewStatus']); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertEquals('NewStatus', $showData['SpcStatus']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createStatus(array $data = []): array + { + $payload = array_merge([ + 'SpcStatus' => 'Status_' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateStatusSuccess() + { + $status = $this->createStatus(); + $id = $status['SpcStaID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SpcStatus' => 'UpdatedStatus']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('UpdatedStatus', $showData['SpcStatus']); + } + + public function testPartialUpdateStatusNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['SpcStatus' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateStatusInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['SpcStatus' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateStatusEmptyPayload() + { + $status = $this->createStatus(); + $id = $status['SpcStaID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateStatusSingleField() + { + $status = $this->createStatus(); + $id = $status['SpcStaID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['SpcStatus' => 'NewStatus']); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertEquals('NewStatus', $showData['SpcStatus']); + } +} diff --git a/tests/feature/Test/TestCreateVariantsTest.php b/tests/feature/Test/TestCreateVariantsTest.php index ce27428..8f228ab 100755 --- a/tests/feature/Test/TestCreateVariantsTest.php +++ b/tests/feature/Test/TestCreateVariantsTest.php @@ -1,679 +1,679 @@ -testModel = new TestDefSiteModel(); - } - - public function testCreateTechnicalWithoutReferenceOrTestMap(): void - { - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type); - } - } - - public function testCreateTechnicalCanAcceptNullSiteAndNumericFields(): void - { - $payload = $this->buildTechnicalPayload('TEST', [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - 'Decimal' => null, - 'Factor' => 2.5, - 'ReqQty' => 1.75, - 'ReqQtyUnit' => 'uL', - ]); - $payload['SiteID'] = null; - $payload['SeqScr'] = null; - $payload['SeqRpt'] = null; - $payload['refnum'] = [ - [ - 'NumRefType' => 'NMRC', - 'RangeType' => 'REF', - 'Sex' => '2', - 'LowSign' => 'GE', - 'Low' => 10, - 'HighSign' => 'LE', - 'High' => 20, - 'AgeStart' => null, - 'AgeEnd' => null, - 'Flag' => 'N', - 'Interpretation' => 'Nullable range', - 'SpcType' => 'GEN', - ], - ]; - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - - $testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId']; - $show = $this->call('get', $this->endpoint . '/' . $testSiteId); - $show->assertStatus(200); - - $data = json_decode($show->getJSON(), true)['data']; - $this->assertNull($data['SiteID']); - $this->assertNull($data['SeqScr']); - $this->assertNull($data['SeqRpt']); - $this->assertNull($data['Decimal']); - $this->assertSame(2.5, (float) $data['Factor']); - $this->assertSame(1.75, (float) $data['ReqQty']); - $this->assertNull($data['refnum'][0]['AgeStart']); - $this->assertNull($data['refnum'][0]['AgeEnd']); - } - - public function testCreateTechnicalWithNumericReference(): void - { - $refnum = $this->buildRefNumEntries('NMRC', true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ], $refnum); - } - } - - public function testCreateTechnicalNumericReferenceReturnsAgeInDays(): void - { - $payload = $this->buildTechnicalPayload('TEST', [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ]); - - $payload['refnum'] = [ - [ - 'NumRefType' => 'NMRC', - 'RangeType' => 'REF', - 'Sex' => '2', - 'LowSign' => 'GE', - 'Low' => 10, - 'HighSign' => 'LE', - 'High' => 20, - 'AgeStart' => 6570, - 'AgeEnd' => 36135, - 'Flag' => 'N', - 'Interpretation' => 'Adult range in days', - 'SpcType' => 'GEN', - ], - ]; - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - - $testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId']; - $show = $this->call('get', $this->endpoint . '/' . $testSiteId); - $show->assertStatus(200); - - $data = json_decode($show->getJSON(), true)['data']; - $this->assertSame(6570, (int) $data['refnum'][0]['AgeStart']); - $this->assertSame(36135, (int) $data['refnum'][0]['AgeEnd']); - } - - public function testNumericRefRangeNotesPersistAfterCreate(): void - { - $notes = 'Auto note ' . uniqid(); - $refnum = $this->buildRefNumEntries('NMRC', false); - $refnum[0]['Notes'] = $notes; - - $payload = $this->buildTechnicalPayload('TEST', [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ]); - $payload['refnum'] = $refnum; - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - $json = json_decode($response->getJSON(), true); - $testSiteId = $json['data']['TestSiteId']; - - $show = $this->call('get', $this->endpoint . '/' . $testSiteId); - $show->assertStatus(200); - $showJson = json_decode($show->getJSON(), true); - $this->assertSame($notes, $showJson['data']['refnum'][0]['Notes']); - } - - public function testNumericRefRangeSpcTypePersistAfterCreate(): void - { - $spcType = 'EDTA'; - $refnum = $this->buildRefNumEntries('NMRC', false); - $refnum[0]['SpcType'] = $spcType; - - $payload = $this->buildTechnicalPayload('TEST', [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ]); - $payload['refnum'] = $refnum; - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - $json = json_decode($response->getJSON(), true); - $testSiteId = $json['data']['TestSiteId']; - - $show = $this->call('get', $this->endpoint . '/' . $testSiteId); - $show->assertStatus(200); - $showJson = json_decode($show->getJSON(), true); - $this->assertSame($spcType, $showJson['data']['refnum'][0]['SpcType']); - } - - public function testCreateTechnicalWithThresholdReference(): void - { - $refnum = $this->buildRefNumEntries('THOLD', true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'NMRIC', - 'RefType' => 'THOLD', - ], $refnum); - } - } - - public function testCreateTechnicalWithTextReference(): void - { - $reftxt = $this->buildRefTxtEntries('TEXT', true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'TEXT', - 'RefType' => 'TEXT', - ], null, $reftxt); - } - } - - public function testCreateTechnicalWithValuesetReference(): void - { - $reftxt = $this->buildRefTxtEntries('VSET', true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'VSET', - 'RefType' => 'VSET', - ], null, $reftxt); - } - } - - public function testCreateTechnicalWithNumericReferenceAndTestMap(): void - { - $refnum = $this->buildRefNumEntries('NMRC', true); - $testmap = $this->buildTestMap(true, true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ], $refnum, null, $testmap); - } - } - - public function testCreateTechnicalWithThresholdReferenceAndTestMap(): void - { - $refnum = $this->buildRefNumEntries('THOLD', true); - $testmap = $this->buildTestMap(true, true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'NMRIC', - 'RefType' => 'THOLD', - ], $refnum, null, $testmap); - } - } - - public function testCreateTechnicalWithTextReferenceAndTestMap(): void - { - $reftxt = $this->buildRefTxtEntries('TEXT', true); - $testmap = $this->buildTestMap(true, true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'TEXT', - 'RefType' => 'TEXT', - ], null, $reftxt, $testmap); - } - } - - public function testCreateTechnicalWithValuesetReferenceAndTestMap(): void - { - $reftxt = $this->buildRefTxtEntries('VSET', true); - $testmap = $this->buildTestMap(true, true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'VSET', - 'RefType' => 'VSET', - ], null, $reftxt, $testmap); - } - } - - public function testCreateTechnicalValuesetWithoutReferenceButWithMap(): void - { - $testmap = $this->buildTestMap(false, true); - foreach (['TEST', 'PARAM'] as $type) { - $this->assertTechnicalCreated($type, [ - 'ResultType' => 'VSET', - 'RefType' => 'VSET', - ], null, null, $testmap); - } - } - - public function testCreateTechnicalWithFlatTestMapPayload(): void - { - $testCode = $this->generateTestCode('TEST'); - $payload = [ - 'SiteID' => self::SITE_ID, - 'TestSiteCode' => $testCode, - 'TestSiteName' => 'Auto Flat Map Create', - 'TestType' => 'TEST', - 'isVisibleScr' => 1, - 'isVisibleRpt' => 1, - 'isCountStat' => 1, - 'details' => [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ], - 'testmap' => $this->buildFlatTestMap($testCode), - ]; - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - - $createdId = json_decode($response->getJSON(), true)['data']['TestSiteId']; - $show = $this->call('get', $this->endpoint . '/' . $createdId); - $show->assertStatus(200); - - $data = json_decode($show->getJSON(), true)['data']; - $this->assertArrayNotHasKey('testmap', $data); - } - - public function testPatchTechnicalWithFlatTestMapPayload(): void - { - $payload = $this->buildTechnicalPayload('TEST', [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ]); - - $create = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $create->assertStatus(201); - - $createJson = json_decode($create->getJSON(), true); - $testSiteId = $createJson['data']['TestSiteId']; - $testCode = $payload['TestSiteCode']; - - $patchPayload = [ - 'details' => [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - ], - 'testmap' => $this->buildFlatTestMap($testCode), - ]; - - $patch = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/' . $testSiteId, $patchPayload); - $patch->assertStatus(200); - - $show = $this->call('get', $this->endpoint . '/' . $testSiteId); - $show->assertStatus(200); - - $data = json_decode($show->getJSON(), true)['data']; - $this->assertArrayNotHasKey('testmap', $data); - } - - public function testCreateCalculatedTestWithoutReferenceOrMap(): void - { - $this->assertCalculatedCreated(false); - } - - public function testCreateCalculatedTestWithReferenceAndTestMap(): void - { - $refnum = $this->buildRefNumEntries('NMRC', true); - $testmap = $this->buildTestMap(true, true); - $members = $this->resolveMemberIds(['GLU', 'CREA']); - $this->assertCalculatedCreated(true, $refnum, $testmap, $members); - } - - public function testCreateGroupTestWithMembers(): void - { - $members = $this->resolveMemberIds(['GLU', 'CREA']); - $testmap = $this->buildTestMap(true, true); - $payload = $this->buildGroupPayload($members, $testmap); - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - $response->assertJSONFragment([ - 'status' => 'created', - 'message' => 'Test created successfully', - ]); - - $json = json_decode($response->getJSON(), true); - $this->assertArrayHasKey('data', $json); - $this->assertArrayHasKey('TestSiteId', $json['data']); - $this->assertIsInt($json['data']['TestSiteId']); - - $show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']); - $show->assertStatus(200); - - $showData = json_decode($show->getJSON(), true)['data']; - $this->assertArrayHasKey('testdefgrp', $showData); - $this->assertArrayHasKey('members', $showData['testdefgrp']); - $this->assertArrayNotHasKey('members', $showData); - - if ($members !== []) { - $this->assertCount(count($members), $showData['testdefgrp']['members']); - } - } - - private function assertTechnicalCreated( - string $type, - array $details = [], - ?array $refnum = null, - ?array $reftxt = null, - ?array $testmap = null - ): void { - $payload = $this->buildTechnicalPayload($type, $details); - if ($refnum !== null) { - $payload['refnum'] = $refnum; - } - if ($reftxt !== null) { - $payload['reftxt'] = $reftxt; - } - if ($testmap !== null) { - $payload['testmap'] = $testmap; - } - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - $response->assertJSONFragment([ - 'status' => 'created', - 'message' => 'Test created successfully', - ]); - - $json = json_decode($response->getJSON(), true); - $this->assertArrayHasKey('data', $json); - $this->assertArrayHasKey('TestSiteId', $json['data']); - $this->assertIsInt($json['data']['TestSiteId']); - - } - - private function assertCalculatedCreated( - bool $withDetails, - ?array $refnum = null, - ?array $testmap = null, - array $members = [] - ): void { - $payload = $this->buildCalculatedPayload($members); - - if ($withDetails && $refnum !== null) { - $payload['refnum'] = $refnum; - } - if ($withDetails && $testmap !== null) { - $payload['testmap'] = $testmap; - } - - $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(201); - $response->assertJSONFragment([ - 'status' => 'created', - 'message' => 'Test created successfully', - ]); - - $json = json_decode($response->getJSON(), true); - $this->assertArrayHasKey('data', $json); - $this->assertArrayHasKey('TestSiteId', $json['data']); - $this->assertIsInt($json['data']['TestSiteId']); - - $show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']); - $show->assertStatus(200); - - $showData = json_decode($show->getJSON(), true)['data']; - $this->assertArrayHasKey('testdefgrp', $showData); - $this->assertArrayHasKey('members', $showData['testdefgrp']); - $this->assertArrayNotHasKey('members', $showData); - $this->assertCount(count($members), $showData['testdefgrp']['members']); - } - - private function buildTechnicalPayload(string $testType, array $details = []): array - { - $payload = [ - 'SiteID' => self::SITE_ID, - 'TestSiteCode' => $this->generateTestCode($testType), - 'TestSiteName' => 'Auto ' . strtoupper($testType), - 'TestType' => $testType, - 'SeqScr' => 900, - 'SeqRpt' => 900, - 'isVisibleScr' => 1, - 'isVisibleRpt' => 1, - 'isCountStat' => 1, - ]; - - $payload['details'] = $this->normalizeDetails($details); - - return $payload; - } - - private function buildCalculatedPayload(array $members = []): array - { - $payload = [ - 'SiteID' => self::SITE_ID, - 'TestSiteCode' => $this->generateTestCode('CALC'), - 'TestSiteName' => 'Auto CALC', - 'TestType' => 'CALC', - 'SeqScr' => 1000, - 'SeqRpt' => 1000, - 'isVisibleScr' => 1, - 'isVisibleRpt' => 1, - 'isCountStat' => 0, - 'testdefgrp' => [ - 'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members), - ], - 'details' => [ - 'DisciplineID' => 2, - 'DepartmentID' => 2, - 'FormulaCode' => '{GLU} + {CREA}', - ], - ]; - - return $payload; - } - - private function buildGroupPayload(array $members, array $testmap): array - { - return [ - 'SiteID' => self::SITE_ID, - 'TestSiteCode' => $this->generateTestCode('PANEL'), - 'TestSiteName' => 'Auto Group', - 'TestType' => 'GROUP', - 'SeqScr' => 300, - 'SeqRpt' => 300, - 'isVisibleScr' => 1, - 'isVisibleRpt' => 1, - 'isCountStat' => 1, - 'testdefgrp' => [ - 'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members), - ], - 'testmap' => $testmap, - ]; - } - - private function normalizeDetails(array $details): array - { - $normalized = [ - 'DisciplineID' => $details['DisciplineID'] ?? 2, - 'DepartmentID' => $details['DepartmentID'] ?? 2, - 'Method' => $details['Method'] ?? 'Automated test', - 'Unit1' => $details['Unit1'] ?? 'mg/dL', - 'Decimal' => array_key_exists('Decimal', $details) ? $details['Decimal'] : 0, - ]; - - foreach (['ResultType', 'RefType', 'FormulaCode', 'ExpectedTAT', 'Factor', 'ReqQty', 'ReqQtyUnit', 'Unit2', 'VSet', 'CollReq'] as $key) { - if (array_key_exists($key, $details)) { - $normalized[$key] = $details[$key]; - } - } - - return $normalized; - } - - private function buildRefNumEntries(string $numRefType, bool $multiple = false): array - { - $rangeType = $numRefType === 'THOLD' ? 'PANIC' : 'REF'; - $entries = [ - [ - 'NumRefType' => $numRefType, - 'RangeType' => $rangeType, - 'Sex' => '2', - 'LowSign' => 'GE', - 'Low' => 10, - 'HighSign' => 'LE', - 'High' => $numRefType === 'THOLD' ? 40 : 20, - 'AgeStart' => 0, - 'AgeEnd' => 43800, - 'Flag' => 'N', - 'Interpretation' => 'Normal range', - 'SpcType' => 'GEN', - 'Notes' => 'Default numeric range note', - ], - ]; - - if ($multiple) { - $entries[] = [ - 'NumRefType' => $numRefType, - 'RangeType' => $rangeType, - 'Sex' => '1', - 'LowSign' => '>', - 'Low' => 5, - 'HighSign' => '<', - 'High' => $numRefType === 'THOLD' ? 50 : 15, - 'AgeStart' => 0, - 'AgeEnd' => 36135, - 'Flag' => 'N', - 'Interpretation' => 'Alternate range', - 'SpcType' => 'GEN', - 'Notes' => 'Alternate numeric range note', - ]; - } - - return $entries; - } - - private function buildRefTxtEntries(string $txtRefType, bool $multiple = false): array - { - $entries = [ - [ - 'SpcType' => 'GEN', - 'TxtRefType' => $txtRefType, - 'Sex' => '2', - 'AgeStart' => 0, - 'AgeEnd' => 43800, - 'RefTxt' => $txtRefType === 'VSET' ? 'NORM=Normal;ABN=Abnormal' : 'NORM=Normal', - 'Flag' => 'N', - ], - ]; - - if ($multiple) { - $entries[] = [ - 'SpcType' => 'GEN', - 'TxtRefType' => $txtRefType, - 'Sex' => '1', - 'AgeStart' => 0, - 'AgeEnd' => 43800, - 'RefTxt' => $txtRefType === 'VSET' ? 'HIGH=High;LOW=Low' : 'ABN=Abnormal', - 'Flag' => 'N', - ]; - } - - return $entries; - } - - private function buildTestMap(bool $multipleMaps = false, bool $multipleDetails = false): array - { - $map = [ - [ - 'HostType' => 'SITE', - 'HostID' => '1', - 'ClientType' => 'WST', - 'ClientID' => '1', - 'details' => [ - [ - 'HostTestCode' => 'GLU', - 'HostTestName' => 'Glucose', - 'ConDefID' => 1, - 'ClientTestCode' => 'GLU_C', - 'ClientTestName' => 'Glucose Client', - ], - ], - ], - ]; - - if ($multipleDetails) { - $map[0]['details'][] = [ - 'HostTestCode' => 'CREA', - 'HostTestName' => 'Creatinine', - 'ConDefID' => 2, - 'ClientTestCode' => 'CREA_C', - 'ClientTestName' => 'Creatinine Client', - ]; - } - - if ($multipleMaps) { - $map[] = [ - 'HostType' => 'WST', - 'HostID' => '3', - 'ClientType' => 'INST', - 'ClientID' => '2', - 'details' => [ - [ - 'HostTestCode' => 'HB', - 'HostTestName' => 'Hemoglobin', - 'ConDefID' => 3, - 'ClientTestCode' => 'HB_C', - 'ClientTestName' => 'Hemoglobin Client', - ], - ], - ]; - } - - return $map; - } - - private function buildFlatTestMap(string $testCode): array - { - return [ - [ - 'HostType' => 'HIS', - 'HostID' => 'LOKAL', - 'HostTestCode' => $testCode, - 'HostTestName' => 'Host ' . $testCode, - 'ClientType' => 'SITE', - 'ClientID' => '1', - 'ClientTestCode' => $testCode, - 'ClientTestName' => 'Client ' . $testCode, - 'ConDefID' => null, - ], - ]; - } - - private function generateTestCode(string $prefix): string - { - $clean = strtoupper(substr($prefix, 0, 3)); - $suffix = strtoupper(substr(md5((string) microtime(true) . random_int(0, 9999)), 0, 6)); - return substr($clean . $suffix, 0, 10); - } - - private function resolveMemberIds(array $codes): array - { - $ids = []; - foreach ($codes as $code) { - $row = $this->testModel->where('TestSiteCode', $code)->where('EndDate IS NULL')->first(); - $this->assertNotEmpty($row, "Seeded test code {$code} not found"); - $ids[] = (int) $row['TestSiteID']; - } - return $ids; - } -} +testModel = new TestDefSiteModel(); + } + + public function testCreateTechnicalWithoutReferenceOrTestMap(): void + { + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type); + } + } + + public function testCreateTechnicalCanAcceptNullSiteAndNumericFields(): void + { + $payload = $this->buildTechnicalPayload('TEST', [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + 'Decimal' => null, + 'Factor' => 2.5, + 'ReqQty' => 1.75, + 'ReqQtyUnit' => 'uL', + ]); + $payload['SiteID'] = null; + $payload['SeqScr'] = null; + $payload['SeqRpt'] = null; + $payload['refnum'] = [ + [ + 'NumRefType' => 'NMRC', + 'RangeType' => 'REF', + 'Sex' => '2', + 'LowSign' => 'GE', + 'Low' => 10, + 'HighSign' => 'LE', + 'High' => 20, + 'AgeStart' => null, + 'AgeEnd' => null, + 'Flag' => 'N', + 'Interpretation' => 'Nullable range', + 'SpcType' => 'GEN', + ], + ]; + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + + $testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId']; + $show = $this->call('get', $this->endpoint . '/' . $testSiteId); + $show->assertStatus(200); + + $data = json_decode($show->getJSON(), true)['data']; + $this->assertNull($data['SiteID']); + $this->assertNull($data['SeqScr']); + $this->assertNull($data['SeqRpt']); + $this->assertNull($data['Decimal']); + $this->assertSame(2.5, (float) $data['Factor']); + $this->assertSame(1.75, (float) $data['ReqQty']); + $this->assertNull($data['refnum'][0]['AgeStart']); + $this->assertNull($data['refnum'][0]['AgeEnd']); + } + + public function testCreateTechnicalWithNumericReference(): void + { + $refnum = $this->buildRefNumEntries('NMRC', true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ], $refnum); + } + } + + public function testCreateTechnicalNumericReferenceReturnsAgeInDays(): void + { + $payload = $this->buildTechnicalPayload('TEST', [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ]); + + $payload['refnum'] = [ + [ + 'NumRefType' => 'NMRC', + 'RangeType' => 'REF', + 'Sex' => '2', + 'LowSign' => 'GE', + 'Low' => 10, + 'HighSign' => 'LE', + 'High' => 20, + 'AgeStart' => 6570, + 'AgeEnd' => 36135, + 'Flag' => 'N', + 'Interpretation' => 'Adult range in days', + 'SpcType' => 'GEN', + ], + ]; + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + + $testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId']; + $show = $this->call('get', $this->endpoint . '/' . $testSiteId); + $show->assertStatus(200); + + $data = json_decode($show->getJSON(), true)['data']; + $this->assertSame(6570, (int) $data['refnum'][0]['AgeStart']); + $this->assertSame(36135, (int) $data['refnum'][0]['AgeEnd']); + } + + public function testNumericRefRangeNotesPersistAfterCreate(): void + { + $notes = 'Auto note ' . uniqid(); + $refnum = $this->buildRefNumEntries('NMRC', false); + $refnum[0]['Notes'] = $notes; + + $payload = $this->buildTechnicalPayload('TEST', [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ]); + $payload['refnum'] = $refnum; + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + $json = json_decode($response->getJSON(), true); + $testSiteId = $json['data']['TestSiteId']; + + $show = $this->call('get', $this->endpoint . '/' . $testSiteId); + $show->assertStatus(200); + $showJson = json_decode($show->getJSON(), true); + $this->assertSame($notes, $showJson['data']['refnum'][0]['Notes']); + } + + public function testNumericRefRangeSpcTypePersistAfterCreate(): void + { + $spcType = 'EDTA'; + $refnum = $this->buildRefNumEntries('NMRC', false); + $refnum[0]['SpcType'] = $spcType; + + $payload = $this->buildTechnicalPayload('TEST', [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ]); + $payload['refnum'] = $refnum; + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + $json = json_decode($response->getJSON(), true); + $testSiteId = $json['data']['TestSiteId']; + + $show = $this->call('get', $this->endpoint . '/' . $testSiteId); + $show->assertStatus(200); + $showJson = json_decode($show->getJSON(), true); + $this->assertSame($spcType, $showJson['data']['refnum'][0]['SpcType']); + } + + public function testCreateTechnicalWithThresholdReference(): void + { + $refnum = $this->buildRefNumEntries('THOLD', true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'NMRIC', + 'RefType' => 'THOLD', + ], $refnum); + } + } + + public function testCreateTechnicalWithTextReference(): void + { + $reftxt = $this->buildRefTxtEntries('TEXT', true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'TEXT', + 'RefType' => 'TEXT', + ], null, $reftxt); + } + } + + public function testCreateTechnicalWithValuesetReference(): void + { + $reftxt = $this->buildRefTxtEntries('VSET', true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'VSET', + 'RefType' => 'VSET', + ], null, $reftxt); + } + } + + public function testCreateTechnicalWithNumericReferenceAndTestMap(): void + { + $refnum = $this->buildRefNumEntries('NMRC', true); + $testmap = $this->buildTestMap(true, true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ], $refnum, null, $testmap); + } + } + + public function testCreateTechnicalWithThresholdReferenceAndTestMap(): void + { + $refnum = $this->buildRefNumEntries('THOLD', true); + $testmap = $this->buildTestMap(true, true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'NMRIC', + 'RefType' => 'THOLD', + ], $refnum, null, $testmap); + } + } + + public function testCreateTechnicalWithTextReferenceAndTestMap(): void + { + $reftxt = $this->buildRefTxtEntries('TEXT', true); + $testmap = $this->buildTestMap(true, true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'TEXT', + 'RefType' => 'TEXT', + ], null, $reftxt, $testmap); + } + } + + public function testCreateTechnicalWithValuesetReferenceAndTestMap(): void + { + $reftxt = $this->buildRefTxtEntries('VSET', true); + $testmap = $this->buildTestMap(true, true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'VSET', + 'RefType' => 'VSET', + ], null, $reftxt, $testmap); + } + } + + public function testCreateTechnicalValuesetWithoutReferenceButWithMap(): void + { + $testmap = $this->buildTestMap(false, true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'VSET', + 'RefType' => 'VSET', + ], null, null, $testmap); + } + } + + public function testCreateTechnicalWithFlatTestMapPayload(): void + { + $testCode = $this->generateTestCode('TEST'); + $payload = [ + 'SiteID' => self::SITE_ID, + 'TestSiteCode' => $testCode, + 'TestSiteName' => 'Auto Flat Map Create', + 'TestType' => 'TEST', + 'isVisibleScr' => 1, + 'isVisibleRpt' => 1, + 'isCountStat' => 1, + 'details' => [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ], + 'testmap' => $this->buildFlatTestMap($testCode), + ]; + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + + $createdId = json_decode($response->getJSON(), true)['data']['TestSiteId']; + $show = $this->call('get', $this->endpoint . '/' . $createdId); + $show->assertStatus(200); + + $data = json_decode($show->getJSON(), true)['data']; + $this->assertArrayNotHasKey('testmap', $data); + } + + public function testPatchTechnicalWithFlatTestMapPayload(): void + { + $payload = $this->buildTechnicalPayload('TEST', [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ]); + + $create = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $create->assertStatus(201); + + $createJson = json_decode($create->getJSON(), true); + $testSiteId = $createJson['data']['TestSiteId']; + $testCode = $payload['TestSiteCode']; + + $patchPayload = [ + 'details' => [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ], + 'testmap' => $this->buildFlatTestMap($testCode), + ]; + + $patch = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/' . $testSiteId, $patchPayload); + $patch->assertStatus(200); + + $show = $this->call('get', $this->endpoint . '/' . $testSiteId); + $show->assertStatus(200); + + $data = json_decode($show->getJSON(), true)['data']; + $this->assertArrayNotHasKey('testmap', $data); + } + + public function testCreateCalculatedTestWithoutReferenceOrMap(): void + { + $this->assertCalculatedCreated(false); + } + + public function testCreateCalculatedTestWithReferenceAndTestMap(): void + { + $refnum = $this->buildRefNumEntries('NMRC', true); + $testmap = $this->buildTestMap(true, true); + $members = $this->resolveMemberIds(['GLU', 'CREA']); + $this->assertCalculatedCreated(true, $refnum, $testmap, $members); + } + + public function testCreateGroupTestWithMembers(): void + { + $members = $this->resolveMemberIds(['GLU', 'CREA']); + $testmap = $this->buildTestMap(true, true); + $payload = $this->buildGroupPayload($members, $testmap); + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + $response->assertJSONFragment([ + 'status' => 'success', + 'message' => 'Test created successfully', + ]); + + $json = json_decode($response->getJSON(), true); + $this->assertArrayHasKey('data', $json); + $this->assertArrayHasKey('TestSiteId', $json['data']); + $this->assertIsInt($json['data']['TestSiteId']); + + $show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']); + $show->assertStatus(200); + + $showData = json_decode($show->getJSON(), true)['data']; + $this->assertArrayHasKey('testdefgrp', $showData); + $this->assertArrayHasKey('members', $showData['testdefgrp']); + $this->assertArrayNotHasKey('members', $showData); + + if ($members !== []) { + $this->assertCount(count($members), $showData['testdefgrp']['members']); + } + } + + private function assertTechnicalCreated( + string $type, + array $details = [], + ?array $refnum = null, + ?array $reftxt = null, + ?array $testmap = null + ): void { + $payload = $this->buildTechnicalPayload($type, $details); + if ($refnum !== null) { + $payload['refnum'] = $refnum; + } + if ($reftxt !== null) { + $payload['reftxt'] = $reftxt; + } + if ($testmap !== null) { + $payload['testmap'] = $testmap; + } + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + $response->assertJSONFragment([ + 'status' => 'success', + 'message' => 'Test created successfully', + ]); + + $json = json_decode($response->getJSON(), true); + $this->assertArrayHasKey('data', $json); + $this->assertArrayHasKey('TestSiteId', $json['data']); + $this->assertIsInt($json['data']['TestSiteId']); + + } + + private function assertCalculatedCreated( + bool $withDetails, + ?array $refnum = null, + ?array $testmap = null, + array $members = [] + ): void { + $payload = $this->buildCalculatedPayload($members); + + if ($withDetails && $refnum !== null) { + $payload['refnum'] = $refnum; + } + if ($withDetails && $testmap !== null) { + $payload['testmap'] = $testmap; + } + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + $response->assertStatus(201); + $response->assertJSONFragment([ + 'status' => 'success', + 'message' => 'Test created successfully', + ]); + + $json = json_decode($response->getJSON(), true); + $this->assertArrayHasKey('data', $json); + $this->assertArrayHasKey('TestSiteId', $json['data']); + $this->assertIsInt($json['data']['TestSiteId']); + + $show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']); + $show->assertStatus(200); + + $showData = json_decode($show->getJSON(), true)['data']; + $this->assertArrayHasKey('testdefgrp', $showData); + $this->assertArrayHasKey('members', $showData['testdefgrp']); + $this->assertArrayNotHasKey('members', $showData); + $this->assertCount(count($members), $showData['testdefgrp']['members']); + } + + private function buildTechnicalPayload(string $testType, array $details = []): array + { + $payload = [ + 'SiteID' => self::SITE_ID, + 'TestSiteCode' => $this->generateTestCode($testType), + 'TestSiteName' => 'Auto ' . strtoupper($testType), + 'TestType' => $testType, + 'SeqScr' => 900, + 'SeqRpt' => 900, + 'isVisibleScr' => 1, + 'isVisibleRpt' => 1, + 'isCountStat' => 1, + ]; + + $payload['details'] = $this->normalizeDetails($details); + + return $payload; + } + + private function buildCalculatedPayload(array $members = []): array + { + $payload = [ + 'SiteID' => self::SITE_ID, + 'TestSiteCode' => $this->generateTestCode('CALC'), + 'TestSiteName' => 'Auto CALC', + 'TestType' => 'CALC', + 'SeqScr' => 1000, + 'SeqRpt' => 1000, + 'isVisibleScr' => 1, + 'isVisibleRpt' => 1, + 'isCountStat' => 0, + 'testdefgrp' => [ + 'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members), + ], + 'details' => [ + 'DisciplineID' => 2, + 'DepartmentID' => 2, + 'FormulaCode' => '{GLU} + {CREA}', + ], + ]; + + return $payload; + } + + private function buildGroupPayload(array $members, array $testmap): array + { + return [ + 'SiteID' => self::SITE_ID, + 'TestSiteCode' => $this->generateTestCode('PANEL'), + 'TestSiteName' => 'Auto Group', + 'TestType' => 'GROUP', + 'SeqScr' => 300, + 'SeqRpt' => 300, + 'isVisibleScr' => 1, + 'isVisibleRpt' => 1, + 'isCountStat' => 1, + 'testdefgrp' => [ + 'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members), + ], + 'testmap' => $testmap, + ]; + } + + private function normalizeDetails(array $details): array + { + $normalized = [ + 'DisciplineID' => $details['DisciplineID'] ?? 2, + 'DepartmentID' => $details['DepartmentID'] ?? 2, + 'Method' => $details['Method'] ?? 'Automated test', + 'Unit1' => $details['Unit1'] ?? 'mg/dL', + 'Decimal' => array_key_exists('Decimal', $details) ? $details['Decimal'] : 0, + ]; + + foreach (['ResultType', 'RefType', 'FormulaCode', 'ExpectedTAT', 'Factor', 'ReqQty', 'ReqQtyUnit', 'Unit2', 'VSet', 'CollReq'] as $key) { + if (array_key_exists($key, $details)) { + $normalized[$key] = $details[$key]; + } + } + + return $normalized; + } + + private function buildRefNumEntries(string $numRefType, bool $multiple = false): array + { + $rangeType = $numRefType === 'THOLD' ? 'PANIC' : 'REF'; + $entries = [ + [ + 'NumRefType' => $numRefType, + 'RangeType' => $rangeType, + 'Sex' => '2', + 'LowSign' => 'GE', + 'Low' => 10, + 'HighSign' => 'LE', + 'High' => $numRefType === 'THOLD' ? 40 : 20, + 'AgeStart' => 0, + 'AgeEnd' => 43800, + 'Flag' => 'N', + 'Interpretation' => 'Normal range', + 'SpcType' => 'GEN', + 'Notes' => 'Default numeric range note', + ], + ]; + + if ($multiple) { + $entries[] = [ + 'NumRefType' => $numRefType, + 'RangeType' => $rangeType, + 'Sex' => '1', + 'LowSign' => '>', + 'Low' => 5, + 'HighSign' => '<', + 'High' => $numRefType === 'THOLD' ? 50 : 15, + 'AgeStart' => 0, + 'AgeEnd' => 36135, + 'Flag' => 'N', + 'Interpretation' => 'Alternate range', + 'SpcType' => 'GEN', + 'Notes' => 'Alternate numeric range note', + ]; + } + + return $entries; + } + + private function buildRefTxtEntries(string $txtRefType, bool $multiple = false): array + { + $entries = [ + [ + 'SpcType' => 'GEN', + 'TxtRefType' => $txtRefType, + 'Sex' => '2', + 'AgeStart' => 0, + 'AgeEnd' => 43800, + 'RefTxt' => $txtRefType === 'VSET' ? 'NORM=Normal;ABN=Abnormal' : 'NORM=Normal', + 'Flag' => 'N', + ], + ]; + + if ($multiple) { + $entries[] = [ + 'SpcType' => 'GEN', + 'TxtRefType' => $txtRefType, + 'Sex' => '1', + 'AgeStart' => 0, + 'AgeEnd' => 43800, + 'RefTxt' => $txtRefType === 'VSET' ? 'HIGH=High;LOW=Low' : 'ABN=Abnormal', + 'Flag' => 'N', + ]; + } + + return $entries; + } + + private function buildTestMap(bool $multipleMaps = false, bool $multipleDetails = false): array + { + $map = [ + [ + 'HostType' => 'SITE', + 'HostID' => '1', + 'ClientType' => 'WST', + 'ClientID' => '1', + 'details' => [ + [ + 'HostTestCode' => 'GLU', + 'HostTestName' => 'Glucose', + 'ConDefID' => 1, + 'ClientTestCode' => 'GLU_C', + 'ClientTestName' => 'Glucose Client', + ], + ], + ], + ]; + + if ($multipleDetails) { + $map[0]['details'][] = [ + 'HostTestCode' => 'CREA', + 'HostTestName' => 'Creatinine', + 'ConDefID' => 2, + 'ClientTestCode' => 'CREA_C', + 'ClientTestName' => 'Creatinine Client', + ]; + } + + if ($multipleMaps) { + $map[] = [ + 'HostType' => 'WST', + 'HostID' => '3', + 'ClientType' => 'INST', + 'ClientID' => '2', + 'details' => [ + [ + 'HostTestCode' => 'HB', + 'HostTestName' => 'Hemoglobin', + 'ConDefID' => 3, + 'ClientTestCode' => 'HB_C', + 'ClientTestName' => 'Hemoglobin Client', + ], + ], + ]; + } + + return $map; + } + + private function buildFlatTestMap(string $testCode): array + { + return [ + [ + 'HostType' => 'HIS', + 'HostID' => 'LOKAL', + 'HostTestCode' => $testCode, + 'HostTestName' => 'Host ' . $testCode, + 'ClientType' => 'SITE', + 'ClientID' => '1', + 'ClientTestCode' => $testCode, + 'ClientTestName' => 'Client ' . $testCode, + 'ConDefID' => null, + ], + ]; + } + + private function generateTestCode(string $prefix): string + { + $clean = strtoupper(substr($prefix, 0, 3)); + $suffix = strtoupper(substr(md5((string) microtime(true) . random_int(0, 9999)), 0, 6)); + return substr($clean . $suffix, 0, 10); + } + + private function resolveMemberIds(array $codes): array + { + $ids = []; + foreach ($codes as $code) { + $row = $this->testModel->where('TestSiteCode', $code)->where('EndDate IS NULL')->first(); + $this->assertNotEmpty($row, "Seeded test code {$code} not found"); + $ids[] = (int) $row['TestSiteID']; + } + return $ids; + } +} diff --git a/tests/feature/Test/TestMapDetailPatchTest.php b/tests/feature/Test/TestMapDetailPatchTest.php index 3022e5e..978e91a 100755 --- a/tests/feature/Test/TestMapDetailPatchTest.php +++ b/tests/feature/Test/TestMapDetailPatchTest.php @@ -1,140 +1,140 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createTestMapDetail(array $data = []): array - { - $mapResponse = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->mapEndpoint, [ - 'HostType' => 'SITE', - 'HostID' => 1, - 'ClientType' => 'SITE', - 'ClientID' => 1, - ]); - $mapResponse->assertStatus(201); - $mapID = json_decode($mapResponse->getJSON(), true)['data']; - - $payload = array_merge([ - 'TestMapID' => $mapID, - 'HostTestCode' => 'HB', - 'HostTestName' => 'Hemoglobin', - 'ClientTestCode' => '2', - 'ClientTestName' => 'Hemoglobin', - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - $detailID = $decoded['data']; - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$detailID}"); - $show->assertStatus(200); - return json_decode($show->getJSON(), true)['data']; - } - - public function testPartialUpdateTestMapDetailSuccess() - { - $detail = $this->createTestMapDetail(); - $id = $detail['TestMapDetailID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ClientTestName' => 'Updated Detail']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Detail', $showData['ClientTestName']); - $this->assertEquals($detail['HostTestCode'], $showData['HostTestCode']); - } - - public function testPartialUpdateTestMapDetailNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['ClientTestName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateTestMapDetailInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['ClientTestName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateTestMapDetailEmptyPayload() - { - $detail = $this->createTestMapDetail(); - $id = $detail['TestMapDetailID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateTestMapDetailSingleField() - { - $detail = $this->createTestMapDetail(); - $id = $detail['TestMapDetailID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['HostTestCode' => 'HBA1C']); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($detail['HostTestCode'], $showData['HostTestCode']); - $this->assertEquals($detail['ClientTestName'], $showData['ClientTestName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createTestMapDetail(array $data = []): array + { + $mapResponse = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->mapEndpoint, [ + 'HostType' => 'SITE', + 'HostID' => 1, + 'ClientType' => 'SITE', + 'ClientID' => 1, + ]); + $mapResponse->assertStatus(201); + $mapID = json_decode($mapResponse->getJSON(), true)['data']; + + $payload = array_merge([ + 'TestMapID' => $mapID, + 'HostTestCode' => 'HB', + 'HostTestName' => 'Hemoglobin', + 'ClientTestCode' => '2', + 'ClientTestName' => 'Hemoglobin', + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + $detailID = $decoded['data']; + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$detailID}"); + $show->assertStatus(200); + return json_decode($show->getJSON(), true)['data']; + } + + public function testPartialUpdateTestMapDetailSuccess() + { + $detail = $this->createTestMapDetail(); + $id = $detail['TestMapDetailID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ClientTestName' => 'Updated Detail']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Detail', $showData['ClientTestName']); + $this->assertEquals($detail['HostTestCode'], $showData['HostTestCode']); + } + + public function testPartialUpdateTestMapDetailNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['ClientTestName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateTestMapDetailInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['ClientTestName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateTestMapDetailEmptyPayload() + { + $detail = $this->createTestMapDetail(); + $id = $detail['TestMapDetailID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateTestMapDetailSingleField() + { + $detail = $this->createTestMapDetail(); + $id = $detail['TestMapDetailID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['HostTestCode' => 'HBA1C']); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($detail['HostTestCode'], $showData['HostTestCode']); + $this->assertEquals($detail['ClientTestName'], $showData['ClientTestName']); + } +} diff --git a/tests/feature/Test/TestShowResponseTest.php b/tests/feature/Test/TestShowResponseTest.php index a58d8c8..1c4fdff 100755 --- a/tests/feature/Test/TestShowResponseTest.php +++ b/tests/feature/Test/TestShowResponseTest.php @@ -1,38 +1,38 @@ -where('TestSiteCode', 'GLU')->where('EndDate IS NULL')->first(); - if (!$test) { - $test = $model->where('TestType', 'TEST')->where('EndDate IS NULL')->first(); - } - - $this->assertNotEmpty($test, 'No active technical test record found for show endpoint test.'); - - $response = $this->call('get', 'api/test/' . $test['TestSiteID']); - - $response->assertStatus(200); - - $json = json_decode($response->getJSON(), true); - - $this->assertSame('success', $json['status'] ?? null); - $this->assertArrayHasKey('data', $json); - $this->assertArrayNotHasKey('testdeftech', $json['data']); - $this->assertArrayHasKey('TestSiteID', $json['data']); - $this->assertArrayHasKey('ResultType', $json['data']); - } -} +where('TestSiteCode', 'GLU')->where('EndDate IS NULL')->first(); + if (!$test) { + $test = $model->where('TestType', 'TEST')->where('EndDate IS NULL')->first(); + } + + $this->assertNotEmpty($test, 'No active technical test record found for show endpoint test.'); + + $response = $this->call('get', 'api/test/' . $test['TestSiteID']); + + $response->assertStatus(200); + + $json = json_decode($response->getJSON(), true); + + $this->assertSame('success', $json['status'] ?? null); + $this->assertArrayHasKey('data', $json); + $this->assertArrayNotHasKey('testdeftech', $json['data']); + $this->assertArrayHasKey('TestSiteID', $json['data']); + $this->assertArrayHasKey('ResultType', $json['data']); + } +} diff --git a/tests/feature/Test/TestsPatchTest.php b/tests/feature/Test/TestsPatchTest.php index 57221a3..449f0eb 100755 --- a/tests/feature/Test/TestsPatchTest.php +++ b/tests/feature/Test/TestsPatchTest.php @@ -1,121 +1,121 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createTest(array $data = []): array - { - $payload = array_merge([ - 'TestCode' => 'TEST_' . uniqid(), - 'TestName' => 'Test Name ' . uniqid(), - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateTestSuccess() - { - $test = $this->createTest(); - $id = $test['TestID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['TestName' => 'Updated Test']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated Test', $showData['TestName']); - $this->assertEquals($test['TestCode'], $showData['TestCode']); - } - - public function testPartialUpdateTestNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['TestName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateTestInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['TestName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateTestEmptyPayload() - { - $test = $this->createTest(); - $id = $test['TestID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateTestSingleField() - { - $test = $this->createTest(); - $id = $test['TestID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['TestCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($test['TestCode'], $showData['TestCode']); - $this->assertEquals($test['TestName'], $showData['TestName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createTest(array $data = []): array + { + $payload = array_merge([ + 'TestCode' => 'TEST_' . uniqid(), + 'TestName' => 'Test Name ' . uniqid(), + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateTestSuccess() + { + $test = $this->createTest(); + $id = $test['TestID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['TestName' => 'Updated Test']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated Test', $showData['TestName']); + $this->assertEquals($test['TestCode'], $showData['TestCode']); + } + + public function testPartialUpdateTestNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['TestName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateTestInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['TestName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateTestEmptyPayload() + { + $test = $this->createTest(); + $id = $test['TestID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateTestSingleField() + { + $test = $this->createTest(); + $id = $test['TestID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['TestCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($test['TestCode'], $showData['TestCode']); + $this->assertEquals($test['TestName'], $showData['TestName']); + } +} diff --git a/tests/feature/User/UserPatchTest.php b/tests/feature/User/UserPatchTest.php index ff37fac..3ff6028 100755 --- a/tests/feature/User/UserPatchTest.php +++ b/tests/feature/User/UserPatchTest.php @@ -1,122 +1,122 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - - private function createUser(array $data = []): array - { - $payload = array_merge([ - 'UserCode' => 'USR_' . uniqid(), - 'UserName' => 'Test User ' . uniqid(), - 'Email' => 'user_' . uniqid() . '@test.com', - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; - } - - public function testPartialUpdateUserSuccess() - { - $user = $this->createUser(); - $id = $user['UserID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['UserName' => 'Updated User']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('Updated User', $showData['UserName']); - $this->assertEquals($user['UserCode'], $showData['UserCode']); - } - - public function testPartialUpdateUserNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['UserName' => 'Updated']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateUserInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['UserName' => 'Updated']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateUserEmptyPayload() - { - $user = $this->createUser(); - $id = $user['UserID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateUserSingleField() - { - $user = $this->createUser(); - $id = $user['UserID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['UserCode' => 'NEW_' . uniqid()]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertNotEquals($user['UserCode'], $showData['UserCode']); - $this->assertEquals($user['UserName'], $showData['UserName']); - } -} + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + + private function createUser(array $data = []): array + { + $payload = array_merge([ + 'UserCode' => 'USR_' . uniqid(), + 'UserName' => 'Test User ' . uniqid(), + 'Email' => 'user_' . uniqid() . '@test.com', + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $decoded = json_decode($response->getJSON(), true); + return $decoded['data']; + } + + public function testPartialUpdateUserSuccess() + { + $user = $this->createUser(); + $id = $user['UserID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['UserName' => 'Updated User']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('Updated User', $showData['UserName']); + $this->assertEquals($user['UserCode'], $showData['UserCode']); + } + + public function testPartialUpdateUserNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['UserName' => 'Updated']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateUserInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['UserName' => 'Updated']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateUserEmptyPayload() + { + $user = $this->createUser(); + $id = $user['UserID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateUserSingleField() + { + $user = $this->createUser(); + $id = $user['UserID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['UserCode' => 'NEW_' . uniqid()]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertNotEquals($user['UserCode'], $showData['UserCode']); + $this->assertEquals($user['UserName'], $showData['UserName']); + } +} diff --git a/tests/phpunit-bootstrap.php b/tests/phpunit-bootstrap.php index 471792f..303c27d 100755 --- a/tests/phpunit-bootstrap.php +++ b/tests/phpunit-bootstrap.php @@ -1,50 +1,50 @@ -load(); - -$db = Database::connect('tests'); -$forge = Database::forge('tests'); - -$db->query('SET FOREIGN_KEY_CHECKS=0'); -foreach ($db->listTables() as $table) { - $forge->dropTable($table, true); -} -$db->query('SET FOREIGN_KEY_CHECKS=1'); - -$migrationsConfig = config(MigrationsConfig::class); -$migrationRunner = new MigrationRunner($migrationsConfig, 'tests'); -try { - $migrationRunner->latest(); -} catch (DatabaseException $e) { - $message = $e->getMessage(); - if (strpos($message, 'already exists') === false) { - throw $e; - } -} - -$initialBufferLevel = ob_get_level(); -ob_start(); -try { - $seeder = Database::seeder('tests'); - $seeder->setSilent(true)->call('DBSeeder'); -} finally { - while (ob_get_level() > $initialBufferLevel) { - ob_end_clean(); - } -} +load(); + +$db = Database::connect('tests'); +$forge = Database::forge('tests'); + +$db->query('SET FOREIGN_KEY_CHECKS=0'); +foreach ($db->listTables() as $table) { + $forge->dropTable($table, true); +} +$db->query('SET FOREIGN_KEY_CHECKS=1'); + +$migrationsConfig = config(MigrationsConfig::class); +$migrationRunner = new MigrationRunner($migrationsConfig, 'tests'); +try { + $migrationRunner->latest(); +} catch (DatabaseException $e) { + $message = $e->getMessage(); + if (strpos($message, 'already exists') === false) { + throw $e; + } +} + +$initialBufferLevel = ob_get_level(); +ob_start(); +try { + $seeder = Database::seeder('tests'); + $seeder->setSilent(true)->call('DBSeeder'); +} finally { + while (ob_get_level() > $initialBufferLevel) { + ob_end_clean(); + } +}