From aaadd593dd1ff2cb17799622a1dff4589dbf40be Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Mon, 16 Mar 2026 15:58:56 +0700 Subject: [PATCH] feat: require id params for update endpoints --- AGENTS.md | 468 ++-- README.md | 14 +- app/Config/Database.php | 21 +- app/Config/Routes.php | 122 +- app/Controllers/Contact/ContactController.php | 20 +- .../Contact/MedicalSpecialtyController.php | 14 +- .../Contact/OccupationController.php | 14 +- app/Controllers/CounterController.php | 14 +- app/Controllers/EdgeController.php | 8 +- .../EquipmentListController.php | 17 +- app/Controllers/LocationController.php | 18 +- app/Controllers/OrderTestController.php | 35 +- .../Organization/AccountController.php | 14 +- .../Organization/CodingSysController.php | 13 +- .../Organization/DepartmentController.php | 16 +- .../Organization/DisciplineController.php | 12 +- .../Organization/HostAppController.php | 12 +- .../Organization/HostComParaController.php | 13 +- .../Organization/SiteController.php | 28 +- .../Organization/WorkstationController.php | 16 +- app/Controllers/PatVisitController.php | 33 +- app/Controllers/Patient/PatientController.php | 17 +- app/Controllers/ReportController.php | 4 +- app/Controllers/ResultController.php | 8 +- .../Specimen/ContainerDefController.php | 14 +- .../Specimen/SpecimenCollectionController.php | 14 +- .../Specimen/SpecimenController.php | 14 +- .../Specimen/SpecimenPrepController.php | 14 +- .../Specimen/SpecimenStatusController.php | 14 +- app/Controllers/Test/TestMapController.php | 18 +- .../Test/TestMapDetailController.php | 17 +- app/Controllers/Test/TestsController.php | 27 +- app/Controllers/User/UserController.php | 26 +- app/Libraries/TestValidationService.php | 50 +- app/Models/Patient/PatientModel.php | 33 +- app/Models/RefRange/RefTxtModel.php | 31 +- app/Models/Rule/RuleDefModel.php | 24 +- app/Services/AuditService.php | 51 +- docs/test-rule-engine.md | 18 +- phpunit.xml.dist | 10 +- public/api-docs.bundled.yaml | 2099 +++++++++++------ public/api-docs.yaml | 74 +- public/paths/calc.yaml | 4 +- public/paths/contact.yaml | 190 +- public/paths/edge-api.yaml | 6 +- public/paths/equipmentlist.yaml | 150 +- public/paths/locations.yaml | 153 +- public/paths/orders.yaml | 140 +- public/paths/organization.yaml | 473 ++-- public/paths/patient-visits.yaml | 305 ++- public/paths/patients.yaml | 84 +- public/paths/reports.yaml | 8 +- public/paths/results.yaml | 22 +- public/paths/rules.yaml | 36 +- public/paths/specimen.yaml | 237 +- public/paths/testmap.yaml | 256 +- public/paths/tests.yaml | 467 +++- public/paths/users.yaml | 70 +- public/paths/valuesets.yaml | 64 +- .../2020-02-22-222222_example_migration.php | 37 - tests/_support/Traits/CreatesPatients.php | 62 + .../Calculator/CalculatorEndpointTest.php | 148 -- tests/feature/Calculator/CalculatorTest.php | 250 -- tests/feature/ContactControllerTest.php | 8 +- tests/feature/Orders/OrderCreateTest.php | 285 --- .../Organization/CodingSysControllerTest.php | 4 +- .../Organization/HostAppControllerTest.php | 5 + tests/feature/OrganizationControllerTest.php | 8 +- .../PatVisit/PatVisitByPatientTest.php | 15 +- tests/feature/PatVisit/PatVisitCreateTest.php | 25 +- tests/feature/PatVisit/PatVisitDeleteTest.php | 25 +- tests/feature/PatVisit/PatVisitShowTest.php | 15 +- tests/feature/PatVisit/PatVisitUpdateTest.php | 142 +- tests/feature/Patients/PatientCheckTest.php | 15 +- tests/feature/Patients/PatientCreateTest.php | 13 +- tests/feature/Patients/PatientDeleteTest.php | 15 +- tests/feature/Patients/PatientIndexTest.php | 15 +- tests/feature/Patients/PatientShowTest.php | 17 +- tests/feature/Patients/PatientUpdateTest.php | 97 +- tests/feature/SimpleTest.php | 16 - tests/feature/Test/TestCreateVariantsTest.php | 443 ++++ tests/feature/TestsControllerTest.php | 417 ---- tests/feature/UniformShowTest.php | 111 - .../ValueSet/ValueSetControllerTest.php | 4 +- tests/phpunit-bootstrap.php | 46 + tests/unit/Rule/RuleDefModelTest.php | 271 --- .../unit/Rules/RuleEngineMultiActionTest.php | 355 --- .../unit/Rules/RuleExpressionCompileTest.php | 60 - .../unit/Rules/RuleExpressionServiceTest.php | 24 - tests/unit/Rules/RuleExpressionSyntaxTest.php | 346 --- tests/unit/TestDef/TestDefModelsTest.php | 191 -- tests/unit/ValueSet/ValueSetTest.php | 50 +- 92 files changed, 4439 insertions(+), 5260 deletions(-) delete mode 100644 tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php create mode 100644 tests/_support/Traits/CreatesPatients.php delete mode 100644 tests/feature/Calculator/CalculatorEndpointTest.php delete mode 100644 tests/feature/Calculator/CalculatorTest.php delete mode 100644 tests/feature/Orders/OrderCreateTest.php delete mode 100644 tests/feature/SimpleTest.php create mode 100644 tests/feature/Test/TestCreateVariantsTest.php delete mode 100644 tests/feature/TestsControllerTest.php delete mode 100644 tests/feature/UniformShowTest.php create mode 100644 tests/phpunit-bootstrap.php delete mode 100644 tests/unit/Rule/RuleDefModelTest.php delete mode 100644 tests/unit/Rules/RuleEngineMultiActionTest.php delete mode 100644 tests/unit/Rules/RuleExpressionCompileTest.php delete mode 100644 tests/unit/Rules/RuleExpressionServiceTest.php delete mode 100644 tests/unit/Rules/RuleExpressionSyntaxTest.php delete mode 100644 tests/unit/TestDef/TestDefModelsTest.php diff --git a/AGENTS.md b/AGENTS.md index 5f508bc..e536267 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,316 +1,152 @@ -# AGENTS.md - Code Guidelines for CLQMS - -> **CLQMS (Clinical Laboratory Quality Management System)** - A headless REST API backend for clinical laboratory workflows built with CodeIgniter 4. - ---- - -## Build, Test & Lint Commands - -```bash -# Run all tests -./vendor/bin/phpunit - -# Run a specific test file -./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php - -# Run a specific test method -./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php - -# Run tests with coverage -./vendor/bin/phpunit --coverage-html build/logs/html - -# Run tests by suite -./vendor/bin/phpunit --testsuite App - -# Generate scaffolding -php spark make:migration -php spark make:model -php spark make:controller - -# Database migrations -php spark migrate -php spark migrate:rollback -``` - ---- - -## Code Style Guidelines - -### PHP Standards -- **PHP Version**: 8.1+ -- **PSR-4 Autoloading**: `App\` maps to `app/`, `Config\` maps to `app/Config/` -- **PSR-12 Coding Style** (follow where applicable) - -### Naming Conventions - -| Element | Convention | Example | -|---------|-----------|---------| -| Classes | PascalCase | `PatientController` | -| Methods | camelCase | `createPatient()` | -| Properties | snake_case (legacy) / camelCase (new) | `$patient_id` / `$patientId` | -| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | -| Tables | snake_case | `patient_visits` | -| Columns | PascalCase (legacy) | `PatientID`, `NameFirst` | -| JSON fields | PascalCase | `"PatientID": "123"` | - -### Imports & Namespaces -- Fully qualified namespaces at the top -- Group imports: Framework first, then App, then external -- Alphabetical order within groups - -```php -model = new \App\Models\ExampleModel(); - } - - public function index() - { - $data = $this->model->findAll(); - return $this->respond(['status' => 'success', 'data' => $data], 200); - } - - public function create() - { - $data = $this->request->getJSON(true); - $result = $this->model->createWithRelations($data); - return $this->respond(['status' => 'success', 'data' => $result], 201); - } -} -``` - -### Response Format -All API responses use standardized format: - -```php -// Success -return $this->respond([ - 'status' => 'success', - 'message' => 'Operation completed', - 'data' => $data -], 200); - -// Error -return $this->respond([ - 'status' => 'failed', - 'message' => 'Error description', - 'data' => [] -], 400); -``` - -**Note**: Custom `ResponseTrait` automatically converts empty strings to `null`. - -### Error Handling -- Use try-catch for JWT and external calls -- Log errors: `log_message('error', $message)` -- Return structured error responses with appropriate HTTP status codes - -```php -try { - $decoded = JWT::decode($token, new Key($key, 'HS256')); -} catch (\Firebase\JWT\ExpiredException $e) { - return $this->respond(['status' => 'failed', 'message' => 'Token expired'], 401); -} catch (\Exception $e) { - return $this->respond(['status' => 'failed', 'message' => 'Invalid token'], 401); -} -``` - -### Database Operations -- Use CodeIgniter Query Builder or Model methods -- Use `helper('utc')` for UTC date conversion -- Wrap multi-table operations in transactions - -```php -$this->db->transStart(); -// ... database operations -$this->db->transComplete(); - -if ($this->db->transStatus() === false) { - return $this->respond(['status' => 'error', 'message' => 'Transaction failed'], 500); -} -``` - -### Model Patterns -- Extend `BaseModel` for automatic UTC date handling -- Use `checkDbError()` for database error detection - -```php -error(); - if (!empty($error['code'])) { - throw new \Exception("{$context} failed: {$error['code']} - {$error['message']}"); - } - } -} -``` - -### Testing Guidelines - -```php -withBodyFormat('json')->post($this->endpoint, $payload); - $result->assertStatus(201); - } -} -``` - -**Test Naming**: `test` (e.g., `testCreatePatientValidationFail`) - -**Test Status Codes**: 200 (GET/PATCH), 201 (POST), 400 (Validation), 401 (Unauthorized), 404 (Not Found), 500 (Server Error) - -### API Design -- **Base URL**: `/api/` -- **Authentication**: JWT token via HttpOnly cookie -- **Content-Type**: `application/json` -- **Methods**: GET (read), POST (create), PATCH (partial update), DELETE (delete) - -### Routes Pattern -```php -$routes->group('api/patient', function ($routes) { - $routes->get('/', 'Patient\PatientController::index'); - $routes->post('/', 'Patient\PatientController::create'); - $routes->get('(:num)', 'Patient\PatientController::show/$1'); - $routes->patch('/', 'Patient\PatientController::update'); - $routes->delete('/', 'Patient\PatientController::delete'); -}); -``` - -### Security -- Use `auth` filter for protected routes -- Sanitize user inputs -- Use parameterized queries -- Store secrets in `.env`, never commit - ---- - -## Project-Specific Conventions - -### API Documentation Sync -**CRITICAL**: When updating any controller, you MUST also update the corresponding OpenAPI YAML documentation: - -- **Paths**: `public/paths/.yaml` (e.g., `patients.yaml`, `orders.yaml`) -- **Schemas**: `public/components/schemas/.yaml` -- **Main file**: `public/api-docs.yaml` (for tags and schema references) - -**After updating YAML files**, regenerate the bundled documentation: -```bash -node public/bundle-api-docs.js -``` - -This produces `public/api-docs.bundled.yaml` which is used by Swagger UI/Redoc. - -### Controller-to-YAML Mapping -| Controller | YAML Path File | YAML Schema File | -|-----------|----------------|------------------| -| `PatientController` | `paths/patients.yaml` | `components/schemas/patient.yaml` | -| `PatVisitController` | `paths/patient-visits.yaml` | `components/schemas/patient-visit.yaml` | -| `OrderTestController` | `paths/orders.yaml` | `components/schemas/orders.yaml` | -| `SpecimenController` | `paths/specimen.yaml` | `components/schemas/specimen.yaml` | -| `TestsController` | `paths/tests.yaml` | `components/schemas/tests.yaml` | -| `AuthController` | `paths/authentication.yaml` | `components/schemas/authentication.yaml` | -| `ResultController` | `paths/results.yaml` | `components/schemas/*.yaml` | -| `EdgeController` | `paths/edge-api.yaml` | `components/schemas/edge-api.yaml` | -| `LocationController` | `paths/locations.yaml` | `components/schemas/master-data.yaml` | -| `ValueSetController` | `paths/valuesets.yaml` | `components/schemas/valuesets.yaml` | -| `ContactController` | `paths/contact.yaml` | (inline schemas) | - -### Legacy Field Naming -Database uses PascalCase columns: `PatientID`, `NameFirst`, `Birthdate`, `CreatedAt`, `UpdatedAt` - -### ValueSet System -```php -use App\Libraries\Lookups; - -$genders = Lookups::get('gender'); -$label = Lookups::getLabel('gender', '1'); // Returns 'Female' -$options = Lookups::getOptions('gender'); -$labeled = Lookups::transformLabels($data, ['Sex' => 'gender']); -``` - -### Nested Data Handling -For entities with nested data (PatIdt, PatCom, PatAtt): -- Extract nested arrays before filtering -- Use transactions for multi-table operations -- Handle empty/null arrays appropriately - ---- - -## Environment Configuration - -### Database (`.env`) -```ini -database.default.hostname = localhost -database.default.database = clqms01 -database.default.username = root -database.default.password = adminsakti -database.default.DBDriver = MySQLi -``` - -### JWT Secret (`.env`) -```ini -JWT_SECRET = '5pandaNdutNdut' -``` - ---- - -## Additional Notes - -- **API-Only**: No view layer - headless REST API -- **Frontend Agnostic**: Any client can consume these APIs -- **Stateless**: JWT-based authentication per request -- **UTC Dates**: All dates stored in UTC, converted for display - -*© 2025 5Panda Team. Engineering Precision in Clinical Diagnostics.* +# 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. diff --git a/README.md b/README.md index f8a3f56..99b29ec 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,9 @@ All API endpoints follow REST conventions: | Method | Endpoint | Description | Auth Required | |--------|----------|-------------|---------------| -| `POST` | `/api/edge/results` | Receive instrument results | API Key | -| `GET` | `/api/edge/orders` | Fetch pending orders | API Key | -| `POST` | `/api/edge/orders/{id}/ack` | Acknowledge order | API Key | +| `POST` | `/api/edge/result` | Receive instrument results | API Key | +| `GET` | `/api/edge/order` | Fetch pending orders | API Key | +| `POST` | `/api/edge/order/{id}/ack` | Acknowledge order | API Key | | `POST` | `/api/edge/status` | Log instrument status | API Key | ### API Response Format @@ -524,15 +524,15 @@ The **Edge API** provides endpoints for integrating laboratory instruments via t | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | `/api/edge/results` | Receive instrument results (stored in `edgeres`) | -| `GET` | `/api/edge/orders` | Fetch pending orders for an instrument | -| `POST` | `/api/edge/orders/:id/ack` | Acknowledge order delivery to instrument | +| `POST` | `/api/edge/result` | Receive instrument results (stored in `edgeres`) | +| `GET` | `/api/edge/order` | Fetch pending orders for an instrument | +| `POST` | `/api/edge/order/:id/ack` | Acknowledge order delivery to instrument | | `POST` | `/api/edge/status` | Log instrument status updates | ### Workflow ``` -Instrument → tiny-edge → POST /api/edge/results → edgeres table → [Manual/Auto Processing] → patres table +Instrument → tiny-edge → POST /api/edge/result → edgeres table → [Manual/Auto Processing] → patres table ``` **Key Features:** diff --git a/app/Config/Database.php b/app/Config/Database.php index f06f471..f0f3466 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -174,7 +174,7 @@ class Database extends Config 'hostname' => 'localhost', 'username' => 'root', 'password' => 'adminsakti', - 'database' => 'clqms01', + 'database' => 'clqms01_test', 'DBDriver' => 'MySQLi', 'DBPrefix' => '', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS 'pConnect' => false, @@ -196,15 +196,18 @@ class Database extends Config ], ]; - public function __construct() - { - parent::__construct(); + public function __construct() + { + parent::__construct(); // Ensure that we always set the database group to 'tests' if // we are currently running an automated test suite, so that // we don't overwrite live data on accident. - if (ENVIRONMENT === 'testing') { - $this->defaultGroup = 'tests'; - } - } -} + if (ENVIRONMENT === 'testing') { + if ($this->tests['database'] === $this->default['database']) { + throw new \RuntimeException('Tests database cannot match the default database.'); + } + $this->defaultGroup = 'tests'; + } + } +} diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d957a0c..89974c7 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -17,16 +17,16 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) { $routes->get('dashboard', 'DashboardController::index'); $routes->get('sample', 'SampleController::index'); - // Results CRUD - $routes->group('results', function ($routes) { - $routes->get('/', 'ResultController::index'); - $routes->get('(:num)', 'ResultController::show/$1'); - $routes->patch('(:num)', 'ResultController::update/$1'); - $routes->delete('(:num)', 'ResultController::delete/$1'); - }); + // Results CRUD + $routes->group('result', function ($routes) { + $routes->get('/', 'ResultController::index'); + $routes->get('(:num)', 'ResultController::show/$1'); + $routes->patch('(:num)', 'ResultController::update/$1'); + $routes->delete('(:num)', 'ResultController::delete/$1'); + }); - // Reports - $routes->get('reports/(:num)', 'ReportController::view/$1'); + // Reports + $routes->get('report/(:num)', 'ReportController::view/$1'); }); @@ -58,7 +58,7 @@ $routes->group('api', function ($routes) { $routes->post('/', 'Patient\PatientController::create'); $routes->get('(:num)', 'Patient\PatientController::show/$1'); $routes->delete('/', 'Patient\PatientController::delete'); - $routes->patch('/', 'Patient\PatientController::update'); + $routes->patch('(:num)', 'Patient\PatientController::update/$1'); $routes->get('check', 'Patient\PatientController::patientCheck'); }); @@ -69,14 +69,14 @@ $routes->group('api', function ($routes) { $routes->get('patient/(:num)', 'PatVisitController::showByPatient/$1'); $routes->get('(:any)', 'PatVisitController::show/$1'); $routes->delete('/', 'PatVisitController::delete'); - $routes->patch('/', 'PatVisitController::update'); + $routes->patch('(:any)', 'PatVisitController::update/$1'); }); $routes->group('patvisitadt', function ($routes) { $routes->get('visit/(:num)', 'PatVisitController::getADTByVisit/$1'); $routes->get('(:num)', 'PatVisitController::showADT/$1'); $routes->post('/', 'PatVisitController::createADT'); - $routes->patch('/', 'PatVisitController::updateADT'); + $routes->patch('(:num)', 'PatVisitController::updateADT/$1'); $routes->delete('/', 'PatVisitController::deleteADT'); }); @@ -87,7 +87,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'LocationController::index'); $routes->get('(:num)', 'LocationController::show/$1'); $routes->post('/', 'LocationController::create'); - $routes->patch('/', 'LocationController::update'); + $routes->patch('(:num)', 'LocationController::update/$1'); $routes->delete('/', 'LocationController::delete'); }); @@ -96,7 +96,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Contact\ContactController::index'); $routes->get('(:num)', 'Contact\ContactController::show/$1'); $routes->post('/', 'Contact\ContactController::create'); - $routes->patch('/', 'Contact\ContactController::update'); + $routes->patch('(:num)', 'Contact\ContactController::update/$1'); $routes->delete('/', 'Contact\ContactController::delete'); }); @@ -104,7 +104,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Contact\OccupationController::index'); $routes->get('(:num)', 'Contact\OccupationController::show/$1'); $routes->post('/', 'Contact\OccupationController::create'); - $routes->patch('/', 'Contact\OccupationController::update'); + $routes->patch('(:num)', 'Contact\OccupationController::update/$1'); //$routes->delete('/', 'Contact\OccupationController::delete'); }); @@ -112,7 +112,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Contact\MedicalSpecialtyController::index'); $routes->get('(:num)', 'Contact\MedicalSpecialtyController::show/$1'); $routes->post('/', 'Contact\MedicalSpecialtyController::create'); - $routes->patch('/', 'Contact\MedicalSpecialtyController::update'); + $routes->patch('(:num)', 'Contact\MedicalSpecialtyController::update/$1'); }); // Lib ValueSet (file-based) @@ -159,7 +159,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'CounterController::index'); $routes->get('(:num)', 'CounterController::show/$1'); $routes->post('/', 'CounterController::create'); - $routes->patch('/', 'CounterController::update'); + $routes->patch('(:num)', 'CounterController::update/$1'); $routes->delete('/', 'CounterController::delete'); }); @@ -177,7 +177,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\AccountController::index'); $routes->get('(:num)', 'Organization\AccountController::show/$1'); $routes->post('/', 'Organization\AccountController::create'); - $routes->patch('/', 'Organization\AccountController::update'); + $routes->patch('(:num)', 'Organization\AccountController::update/$1'); $routes->delete('/', 'Organization\AccountController::delete'); }); @@ -186,7 +186,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\SiteController::index'); $routes->get('(:num)', 'Organization\SiteController::show/$1'); $routes->post('/', 'Organization\SiteController::create'); - $routes->patch('/', 'Organization\SiteController::update'); + $routes->patch('(:num)', 'Organization\SiteController::update/$1'); $routes->delete('/', 'Organization\SiteController::delete'); }); @@ -195,7 +195,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\DisciplineController::index'); $routes->get('(:num)', 'Organization\DisciplineController::show/$1'); $routes->post('/', 'Organization\DisciplineController::create'); - $routes->patch('/', 'Organization\DisciplineController::update'); + $routes->patch('(:num)', 'Organization\DisciplineController::update/$1'); $routes->delete('/', 'Organization\DisciplineController::delete'); }); @@ -204,7 +204,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\DepartmentController::index'); $routes->get('(:num)', 'Organization\DepartmentController::show/$1'); $routes->post('/', 'Organization\DepartmentController::create'); - $routes->patch('/', 'Organization\DepartmentController::update'); + $routes->patch('(:num)', 'Organization\DepartmentController::update/$1'); $routes->delete('/', 'Organization\DepartmentController::delete'); }); @@ -213,7 +213,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\WorkstationController::index'); $routes->get('(:num)', 'Organization\WorkstationController::show/$1'); $routes->post('/', 'Organization\WorkstationController::create'); - $routes->patch('/', 'Organization\WorkstationController::update'); + $routes->patch('(:num)', 'Organization\WorkstationController::update/$1'); $routes->delete('/', 'Organization\WorkstationController::delete'); }); @@ -222,7 +222,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\HostAppController::index'); $routes->get('(:any)', 'Organization\HostAppController::show/$1'); $routes->post('/', 'Organization\HostAppController::create'); - $routes->patch('/', 'Organization\HostAppController::update'); + $routes->patch('(:any)', 'Organization\HostAppController::update/$1'); $routes->delete('/', 'Organization\HostAppController::delete'); }); @@ -231,7 +231,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\HostComParaController::index'); $routes->get('(:any)', 'Organization\HostComParaController::show/$1'); $routes->post('/', 'Organization\HostComParaController::create'); - $routes->patch('/', 'Organization\HostComParaController::update'); + $routes->patch('(:any)', 'Organization\HostComParaController::update/$1'); $routes->delete('/', 'Organization\HostComParaController::delete'); }); @@ -240,7 +240,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Organization\CodingSysController::index'); $routes->get('(:num)', 'Organization\CodingSysController::show/$1'); $routes->post('/', 'Organization\CodingSysController::create'); - $routes->patch('/', 'Organization\CodingSysController::update'); + $routes->patch('(:num)', 'Organization\CodingSysController::update/$1'); $routes->delete('/', 'Organization\CodingSysController::delete'); }); }); @@ -250,18 +250,18 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Infrastructure\EquipmentListController::index'); $routes->get('(:num)', 'Infrastructure\EquipmentListController::show/$1'); $routes->post('/', 'Infrastructure\EquipmentListController::create'); - $routes->patch('/', 'Infrastructure\EquipmentListController::update'); + $routes->patch('(:num)', 'Infrastructure\EquipmentListController::update/$1'); $routes->delete('/', 'Infrastructure\EquipmentListController::delete'); }); - // Users - $routes->group('users', function ($routes) { - $routes->get('/', 'User\UserController::index'); - $routes->get('(:num)', 'User\UserController::show/$1'); - $routes->post('/', 'User\UserController::create'); - $routes->patch('/', 'User\UserController::update'); - $routes->delete('(:num)', 'User\UserController::delete/$1'); - }); + // Users + $routes->group('user', function ($routes) { + $routes->get('/', 'User\UserController::index'); + $routes->get('(:num)', 'User\UserController::show/$1'); + $routes->post('/', 'User\UserController::create'); + $routes->patch('(:num)', 'User\UserController::update/$1'); + $routes->delete('(:num)', 'User\UserController::delete/$1'); + }); // Specimen $routes->group('specimen', function ($routes) { @@ -270,40 +270,40 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Specimen\ContainerDefController::index'); $routes->get('(:num)', 'Specimen\ContainerDefController::show/$1'); $routes->post('/', 'Specimen\ContainerDefController::create'); - $routes->patch('/', 'Specimen\ContainerDefController::update'); + $routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1'); }); $routes->group('containerdef', function ($routes) { $routes->get('/', 'Specimen\ContainerDefController::index'); $routes->get('(:num)', 'Specimen\ContainerDefController::show/$1'); $routes->post('/', 'Specimen\ContainerDefController::create'); - $routes->patch('/', 'Specimen\ContainerDefController::update'); + $routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1'); }); $routes->group('prep', function ($routes) { $routes->get('/', 'Specimen\SpecimenPrepController::index'); $routes->get('(:num)', 'Specimen\SpecimenPrepController::show/$1'); $routes->post('/', 'Specimen\SpecimenPrepController::create'); - $routes->patch('/', 'Specimen\SpecimenPrepController::update'); + $routes->patch('(:num)', 'Specimen\SpecimenPrepController::update/$1'); }); $routes->group('status', function ($routes) { $routes->get('/', 'Specimen\SpecimenStatusController::index'); $routes->get('(:num)', 'Specimen\SpecimenStatusController::show/$1'); $routes->post('/', 'Specimen\SpecimenStatusController::create'); - $routes->patch('/', 'Specimen\SpecimenStatusController::update'); + $routes->patch('(:num)', 'Specimen\SpecimenStatusController::update/$1'); }); $routes->group('collection', function ($routes) { $routes->get('/', 'Specimen\SpecimenCollectionController::index'); $routes->get('(:num)', 'Specimen\SpecimenCollectionController::show/$1'); $routes->post('/', 'Specimen\SpecimenCollectionController::create'); - $routes->patch('/', 'Specimen\SpecimenCollectionController::update'); + $routes->patch('(:num)', 'Specimen\SpecimenCollectionController::update/$1'); }); $routes->get('/', 'Specimen\SpecimenController::index'); $routes->get('(:num)', 'Specimen\SpecimenController::show/$1'); $routes->post('/', 'Specimen\SpecimenController::create'); - $routes->patch('/', 'Specimen\SpecimenController::update'); + $routes->patch('(:num)', 'Specimen\SpecimenController::update/$1'); $routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1'); }); @@ -312,12 +312,12 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Test\TestsController::index'); $routes->get('(:num)', 'Test\TestsController::show/$1'); $routes->post('/', 'Test\TestsController::create'); - $routes->patch('/', 'Test\TestsController::update'); + $routes->patch('(:num)', 'Test\TestsController::update/$1'); $routes->group('testmap', function ($routes) { $routes->get('/', 'Test\TestMapController::index'); $routes->get('(:num)', 'Test\TestMapController::show/$1'); $routes->post('/', 'Test\TestMapController::create'); - $routes->patch('/', 'Test\TestMapController::update'); + $routes->patch('(:num)', 'Test\TestMapController::update/$1'); $routes->delete('/', 'Test\TestMapController::delete'); // Filter routes @@ -328,7 +328,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Test\TestMapDetailController::index'); $routes->get('(:num)', 'Test\TestMapDetailController::show/$1'); $routes->post('/', 'Test\TestMapDetailController::create'); - $routes->patch('/', 'Test\TestMapDetailController::update'); + $routes->patch('(:num)', 'Test\TestMapDetailController::update/$1'); $routes->delete('/', 'Test\TestMapDetailController::delete'); $routes->get('by-testmap/(:num)', 'Test\TestMapDetailController::showByTestMap/$1'); $routes->post('batch', 'Test\TestMapDetailController::batchCreate'); @@ -343,36 +343,36 @@ $routes->group('api', function ($routes) { $routes->get('/', 'OrderTestController::index'); $routes->get('(:any)', 'OrderTestController::show/$1'); $routes->post('/', 'OrderTestController::create'); - $routes->patch('/', 'OrderTestController::update'); + $routes->patch('(:any)', 'OrderTestController::update/$1'); $routes->delete('/', 'OrderTestController::delete'); $routes->post('status', 'OrderTestController::updateStatus'); }); - // Rules - $routes->group('rules', function ($routes) { - $routes->get('/', 'Rule\RuleController::index'); - $routes->get('(:num)', 'Rule\RuleController::show/$1'); - $routes->post('/', 'Rule\RuleController::create'); - $routes->patch('(:num)', 'Rule\RuleController::update/$1'); + // Rules + $routes->group('rule', function ($routes) { + $routes->get('/', 'Rule\RuleController::index'); + $routes->get('(:num)', 'Rule\RuleController::show/$1'); + $routes->post('/', 'Rule\RuleController::create'); + $routes->patch('(:num)', 'Rule\RuleController::update/$1'); $routes->delete('(:num)', 'Rule\RuleController::delete/$1'); $routes->post('validate', 'Rule\RuleController::validateExpr'); $routes->post('compile', 'Rule\RuleController::compile'); }); // Demo/Test Routes (No Auth) -$routes->group('api/demo', function ($routes) { - $routes->post('order', 'Test\DemoOrderController::createDemoOrder'); - $routes->get('orders', 'Test\DemoOrderController::listDemoOrders'); -}); +$routes->group('api/demo', function ($routes) { + $routes->post('order', 'Test\DemoOrderController::createDemoOrder'); + $routes->get('order', 'Test\DemoOrderController::listDemoOrders'); +}); // Edge API - Integration with tiny-edge - $routes->group('edge', function ($routes) { - $routes->post('results', 'EdgeController::results'); - $routes->get('orders', 'EdgeController::orders'); - $routes->post('orders/(:num)/ack', 'EdgeController::ack/$1'); - $routes->post('status', 'EdgeController::status'); - }); -}); + $routes->group('edge', function ($routes) { + $routes->post('result', 'EdgeController::results'); + $routes->get('order', 'EdgeController::orders'); + $routes->post('order/(:num)/ack', 'EdgeController::ack/$1'); + $routes->post('status', 'EdgeController::status'); + }); +}); // Khusus /* diff --git a/app/Controllers/Contact/ContactController.php b/app/Controllers/Contact/ContactController.php index be7b37e..9236fc8 100644 --- a/app/Controllers/Contact/ContactController.php +++ b/app/Controllers/Contact/ContactController.php @@ -75,12 +75,20 @@ class ContactController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } - try { - $this->model->saveContact($input); - $id = $input['ContactID']; + public function update($ContactID = null) { + $input = $this->request->getJSON(true); + if (!$ContactID || !ctype_digit((string) $ContactID)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'ContactID is required and must be a valid integer', + 'data' => [] + ], 400); + } + $input['ContactID'] = (int) $ContactID; + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } + try { + $this->model->saveContact($input); + $id = $input['ContactID']; 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/Contact/MedicalSpecialtyController.php b/app/Controllers/Contact/MedicalSpecialtyController.php index 6aa53d8..4741737 100644 --- a/app/Controllers/Contact/MedicalSpecialtyController.php +++ b/app/Controllers/Contact/MedicalSpecialtyController.php @@ -51,11 +51,15 @@ class MedicalSpecialtyController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - $this->model->update($input['SpecialtyID'], $input); - return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['SpecialtyID'] ], 201); + public function update($SpecialtyID = null) { + $input = $this->request->getJSON(true); + if (!$SpecialtyID || !ctype_digit((string) $SpecialtyID)) { + return $this->respond(['status' => 'error', 'message' => 'SpecialtyID is required and must be a valid integer'], 400); + } + $input['SpecialtyID'] = (int) $SpecialtyID; + try { + $this->model->update($input['SpecialtyID'], $input); + return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['SpecialtyID'] ], 201); } catch (\Throwable $e) { return $this->failServerError('Exception : ' . $e->getMessage()); } diff --git a/app/Controllers/Contact/OccupationController.php b/app/Controllers/Contact/OccupationController.php index fe847ca..33b8cdd 100644 --- a/app/Controllers/Contact/OccupationController.php +++ b/app/Controllers/Contact/OccupationController.php @@ -51,11 +51,15 @@ class OccupationController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - $this->model->update($input['OccupationID'], $input); - return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['OccupationID'] ], 201); + public function update($OccupationID = null) { + $input = $this->request->getJSON(true); + if (!$OccupationID || !ctype_digit((string) $OccupationID)) { + return $this->respond(['status' => 'error', 'message' => 'OccupationID is required and must be a valid integer'], 400); + } + $input['OccupationID'] = (int) $OccupationID; + try { + $this->model->update($input['OccupationID'], $input); + return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['OccupationID'] ], 201); } catch (\Throwable $e) { return $this->failServerError('Exception : ' . $e->getMessage()); } diff --git a/app/Controllers/CounterController.php b/app/Controllers/CounterController.php index 0057726..a70d9fd 100644 --- a/app/Controllers/CounterController.php +++ b/app/Controllers/CounterController.php @@ -43,11 +43,15 @@ class CounterController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - $this->model->update($input['CounterID'], $input); - return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['CounterID'] ], 201); + public function update($CounterID = null) { + $input = $this->request->getJSON(true); + if (!$CounterID || !ctype_digit((string) $CounterID)) { + return $this->respond(['status' => 'error', 'message' => 'CounterID is required and must be a valid integer'], 400); + } + $input['CounterID'] = (int) $CounterID; + try { + $this->model->update($input['CounterID'], $input); + return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['CounterID'] ], 201); } catch (\Throwable $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } diff --git a/app/Controllers/EdgeController.php b/app/Controllers/EdgeController.php index c99b7d6..2e83e3c 100644 --- a/app/Controllers/EdgeController.php +++ b/app/Controllers/EdgeController.php @@ -19,7 +19,7 @@ class EdgeController extends Controller } /** - * POST /api/edge/results + * POST /api/edge/result * Receive results from tiny-edge */ public function results() @@ -70,7 +70,7 @@ class EdgeController extends Controller } /** - * GET /api/edge/orders + * GET /api/edge/order * Return pending orders for an instrument */ public function orders() @@ -96,7 +96,7 @@ class EdgeController extends Controller } /** - * POST /api/edge/orders/:id/ack + * POST /api/edge/order/:id/ack * Acknowledge order delivery */ public function ack($orderId = null) @@ -131,7 +131,7 @@ class EdgeController extends Controller } /** - * POST /api/edge/status + * POST /api/edge/status * Log instrument status */ public function status() diff --git a/app/Controllers/Infrastructure/EquipmentListController.php b/app/Controllers/Infrastructure/EquipmentListController.php index d913168..421b045 100644 --- a/app/Controllers/Infrastructure/EquipmentListController.php +++ b/app/Controllers/Infrastructure/EquipmentListController.php @@ -76,13 +76,16 @@ class EquipmentListController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - - try { - $EID = $input['EID']; - $this->model->update($EID, $input); - return $this->respond([ + public function update($EID = null) { + $input = $this->request->getJSON(true); + + try { + if (!$EID || !ctype_digit((string) $EID)) { + return $this->failValidationErrors('EID is required.'); + } + $input['EID'] = (int) $EID; + $this->model->update($EID, $input); + return $this->respond([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $EID diff --git a/app/Controllers/LocationController.php b/app/Controllers/LocationController.php index ff274ed..c26af65 100644 --- a/app/Controllers/LocationController.php +++ b/app/Controllers/LocationController.php @@ -50,11 +50,19 @@ class LocationController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors()); } - $result = $this->model->saveLocation($input, true); + public function update($LocationID = null) { + $input = $this->request->getJSON(true); + if (!$LocationID || !ctype_digit((string) $LocationID)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'LocationID is required and must be a valid integer', + 'data' => [] + ], 400); + } + $input['LocationID'] = (int) $LocationID; + try { + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors()); } + $result = $this->model->saveLocation($input, true); return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $result ], 201); } catch (\Throwable $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/OrderTestController.php b/app/Controllers/OrderTestController.php index 2a6e6ec..e312100 100644 --- a/app/Controllers/OrderTestController.php +++ b/app/Controllers/OrderTestController.php @@ -191,18 +191,23 @@ class OrderTestController extends Controller { } } - public function update() { - $input = $this->request->getJSON(true); - - if (empty($input['OrderID'])) { - return $this->failValidationErrors(['OrderID' => 'OrderID is required']); - } - - try { - $order = $this->model->getOrder($input['OrderID']); - if (!$order) { - return $this->failNotFound('Order not found'); - } + public function update($OrderID = null) { + $input = $this->request->getJSON(true); + + if ($OrderID === null || $OrderID === '') { + return $this->failValidationErrors(['OrderID' => 'OrderID is required']); + } + + if (isset($input['OrderID']) && (string) $input['OrderID'] !== (string) $OrderID) { + return $this->failValidationErrors(['OrderID' => 'OrderID in URL does not match body']); + } + + try { + $input['OrderID'] = $OrderID; + $order = $this->model->getOrder($OrderID); + if (!$order) { + return $this->failNotFound('Order not found'); + } $updateData = []; if (isset($input['Priority'])) $updateData['Priority'] = $input['Priority']; @@ -215,9 +220,9 @@ class OrderTestController extends Controller { $this->model->update($order['InternalOID'], $updateData); } - $updatedOrder = $this->model->getOrder($input['OrderID']); - $updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']); - $updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']); + $updatedOrder = $this->model->getOrder($OrderID); + $updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']); + $updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']); return $this->respond([ 'status' => 'success', diff --git a/app/Controllers/Organization/AccountController.php b/app/Controllers/Organization/AccountController.php index 4d7ec50..1247220 100644 --- a/app/Controllers/Organization/AccountController.php +++ b/app/Controllers/Organization/AccountController.php @@ -65,11 +65,15 @@ class AccountController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - $id = $input['AccountID']; - if (!$id) { return $this->failValidationErrors('ID is required.'); } + public function update($AccountID = null) { + $input = $this->request->getJSON(true); + if (!$AccountID || !ctype_digit((string) $AccountID)) { + return $this->failValidationErrors('ID is required.'); + } + $input['AccountID'] = (int) $AccountID; + try { + $id = $input['AccountID']; + if (!$id) { return $this->failValidationErrors('ID is required.'); } $this->model->update($id, $input); return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201); } catch (\Throwable $e) { diff --git a/app/Controllers/Organization/CodingSysController.php b/app/Controllers/Organization/CodingSysController.php index b87698b..a0abe02 100644 --- a/app/Controllers/Organization/CodingSysController.php +++ b/app/Controllers/Organization/CodingSysController.php @@ -78,16 +78,21 @@ class CodingSysController extends BaseController { } } - public function update() { + public function update($CodingSysID = null) { $input = $this->request->getJSON(true); try { - $id = $input['CodingSysID'] ?? null; - - if (!$id) { + if (!$CodingSysID || !ctype_digit((string) $CodingSysID)) { return $this->failValidationErrors('CodingSysID is required.'); } + if (isset($input['CodingSysID']) && (string) $input['CodingSysID'] !== (string) $CodingSysID) { + return $this->failValidationErrors('CodingSysID in URL does not match body.'); + } + + $id = (int) $CodingSysID; + $input['CodingSysID'] = $id; + $this->model->update($id, $input); return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201); } catch (\Throwable $e) { diff --git a/app/Controllers/Organization/DepartmentController.php b/app/Controllers/Organization/DepartmentController.php index c4e65ff..6c33301 100644 --- a/app/Controllers/Organization/DepartmentController.php +++ b/app/Controllers/Organization/DepartmentController.php @@ -62,12 +62,16 @@ class DepartmentController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - $id = $input['DepartmentID']; - $this->model->update($id, $input); - return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201); + public function update($DepartmentID = null) { + $input = $this->request->getJSON(true); + try { + if (!$DepartmentID || !ctype_digit((string) $DepartmentID)) { + return $this->failValidationErrors('ID is required.'); + } + $input['DepartmentID'] = (int) $DepartmentID; + $id = $input['DepartmentID']; + $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/DisciplineController.php b/app/Controllers/Organization/DisciplineController.php index b35daef..241729c 100644 --- a/app/Controllers/Organization/DisciplineController.php +++ b/app/Controllers/Organization/DisciplineController.php @@ -63,11 +63,13 @@ class DisciplineController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - $id = $input['DisciplineID']; - $this->model->update($id, $input); - return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201); + public function update($DisciplineID = null) { + $input = $this->request->getJSON(true); + if (!$DisciplineID || !ctype_digit((string) $DisciplineID)) { return $this->failValidationErrors('ID is required.'); } + $input['DisciplineID'] = (int) $DisciplineID; + $id = $input['DisciplineID']; + $this->model->update($id, $input); + return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201); /* try { $id = $input['DisciplineID']; diff --git a/app/Controllers/Organization/HostAppController.php b/app/Controllers/Organization/HostAppController.php index a3ed0f6..9848715 100644 --- a/app/Controllers/Organization/HostAppController.php +++ b/app/Controllers/Organization/HostAppController.php @@ -82,16 +82,20 @@ class HostAppController extends BaseController { } } - public function update() { + public function update($HostAppID = null) { $input = $this->request->getJSON(true); try { - $id = $input['HostAppID'] ?? null; - - if (!$id) { + if ($HostAppID === null || $HostAppID === '') { return $this->failValidationErrors('HostAppID is required.'); } + if (isset($input['HostAppID']) && (string) $input['HostAppID'] !== (string) $HostAppID) { + return $this->failValidationErrors('HostAppID in URL does not match body.'); + } + + $id = $HostAppID; + $input['HostAppID'] = $id; $this->model->update($id, $input); return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201); } catch (\Throwable $e) { diff --git a/app/Controllers/Organization/HostComParaController.php b/app/Controllers/Organization/HostComParaController.php index 462d42d..d2e695e 100644 --- a/app/Controllers/Organization/HostComParaController.php +++ b/app/Controllers/Organization/HostComParaController.php @@ -82,16 +82,21 @@ class HostComParaController extends BaseController { } } - public function update() { + public function update($HostAppID = null) { $input = $this->request->getJSON(true); try { - $id = $input['HostAppID'] ?? null; - - if (!$id) { + if ($HostAppID === null || $HostAppID === '') { return $this->failValidationErrors('HostAppID is required.'); } + if (isset($input['HostAppID']) && (string) $input['HostAppID'] !== (string) $HostAppID) { + return $this->failValidationErrors('HostAppID in URL does not match body.'); + } + + $id = $HostAppID; + $input['HostAppID'] = $id; + $this->model->update($id, $input); return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201); } catch (\Throwable $e) { diff --git a/app/Controllers/Organization/SiteController.php b/app/Controllers/Organization/SiteController.php index cbef1fd..2d884d8 100644 --- a/app/Controllers/Organization/SiteController.php +++ b/app/Controllers/Organization/SiteController.php @@ -58,10 +58,10 @@ class SiteController extends BaseController { $input = $this->request->getJSON(true); $validation = service('validation'); - $validation->setRules([ - 'SiteCode' => 'required|regex_match[/^[A-Z0-9]{2}$/]', - 'SiteName' => 'required', - ]); + $validation->setRules([ + 'SiteCode' => 'required|regex_match[/^[A-Z0-9]{2,6}$/]', + 'SiteName' => 'required', + ]); if (!$validation->run($input)) { return $this->failValidationErrors($validation->getErrors()); @@ -75,17 +75,19 @@ class SiteController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); + public function update($SiteID = null) { + $input = $this->request->getJSON(true); + + if (!$SiteID || !ctype_digit((string) $SiteID)) { return $this->failValidationErrors('ID is required.'); } + $input['SiteID'] = (int) $SiteID; - $id = $input['SiteID']; - if (!$id) { return $this->failValidationErrors('ID is required.'); } + $id = $input['SiteID']; - if (!empty($input['SiteCode'])) { - $validation = service('validation'); - $validation->setRules([ - 'SiteCode' => 'regex_match[/^[A-Z0-9]{2}$/]', - ]); + if (!empty($input['SiteCode'])) { + $validation = service('validation'); + $validation->setRules([ + 'SiteCode' => 'regex_match[/^[A-Z0-9]{2,6}$/]', + ]); if (!$validation->run($input)) { return $this->failValidationErrors($validation->getErrors()); diff --git a/app/Controllers/Organization/WorkstationController.php b/app/Controllers/Organization/WorkstationController.php index bf90973..175c012 100644 --- a/app/Controllers/Organization/WorkstationController.php +++ b/app/Controllers/Organization/WorkstationController.php @@ -63,12 +63,16 @@ class WorkstationController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - $id = $input['WorkstationID']; - $this->model->update($id, $input); - return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201); + public function update($WorkstationID = null) { + $input = $this->request->getJSON(true); + try { + if (!$WorkstationID || !ctype_digit((string) $WorkstationID)) { + return $this->failValidationErrors('ID is required.'); + } + $input['WorkstationID'] = (int) $WorkstationID; + $id = $input['WorkstationID']; + $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/PatVisitController.php b/app/Controllers/PatVisitController.php index 7796a4e..7764747 100644 --- a/app/Controllers/PatVisitController.php +++ b/app/Controllers/PatVisitController.php @@ -94,16 +94,16 @@ class PatVisitController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - try { - if (!isset($input["InternalPVID"]) || !is_numeric($input["InternalPVID"])) { - return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); - } - - // Check if visit exists - $visit = $this->model->find($input["InternalPVID"]); - if (!$visit) { + public function update($InternalPVID = null) { + $input = $this->request->getJSON(true); + if (!$InternalPVID || !is_numeric($InternalPVID)) { + return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); + } + $input['InternalPVID'] = $InternalPVID; + try { + // Check if visit exists + $visit = $this->model->find($input["InternalPVID"]); + if (!$visit) { return $this->respond(['status' => 'error', 'message' => 'Visit not found'], 404); } @@ -174,12 +174,13 @@ class PatVisitController extends BaseController { } } - public function updateADT() { - $input = $this->request->getJSON(true); - if (!$input["PVADTID"] || !is_numeric($input["PVADTID"])) { return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); } - $modelPVA = new PatVisitADTModel(); - try { - $data = $modelPVA->update($input['PVADTID'], $input); + public function updateADT($PVADTID = null) { + $input = $this->request->getJSON(true); + if (!$PVADTID || !is_numeric($PVADTID)) { return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); } + $input['PVADTID'] = $PVADTID; + $modelPVA = new PatVisitADTModel(); + try { + $data = $modelPVA->update($input['PVADTID'], $input); return $this->respond(['status' => 'success', 'message' => 'Data updated successfully', 'data' => $data], 200); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/Patient/PatientController.php b/app/Controllers/Patient/PatientController.php index a517e1b..adf2e31 100644 --- a/app/Controllers/Patient/PatientController.php +++ b/app/Controllers/Patient/PatientController.php @@ -115,8 +115,17 @@ class PatientController extends Controller { } } - public function update() { - $input = $this->request->getJSON(true); + public function update($InternalPID = null) { + $input = $this->request->getJSON(true); + + if (!$InternalPID || !ctype_digit((string) $InternalPID)) { + return $this->respond([ + 'status' => 'error', + 'message' => 'InternalPID is required and must be a valid integer.' + ], 400); + } + + $input['InternalPID'] = (int) $InternalPID; // Khusus untuk Override PATIDT $type = $input['PatIdt']['IdentifierType'] ?? null; @@ -139,8 +148,8 @@ class PatientController extends Controller { if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } try { - $InternalPID = $this->model->updatePatient($input); - return $this->respondCreated([ 'status' => 'success', 'message' => "data $InternalPID update successfully" ]); + $InternalPID = $this->model->updatePatient($input); + return $this->respondCreated([ 'status' => 'success', 'message' => "data $InternalPID update successfully" ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } diff --git a/app/Controllers/ReportController.php b/app/Controllers/ReportController.php index cff11b9..c0ae8b8 100644 --- a/app/Controllers/ReportController.php +++ b/app/Controllers/ReportController.php @@ -23,7 +23,7 @@ class ReportController extends Controller { /** * Generate HTML lab report for an order - * GET /api/reports/{orderID} + * GET /api/report/{orderID} */ public function view($orderID) { try { @@ -72,4 +72,4 @@ class ReportController extends Controller { ], 500); } } -} \ No newline at end of file +} diff --git a/app/Controllers/ResultController.php b/app/Controllers/ResultController.php index 8736af0..c9c6429 100644 --- a/app/Controllers/ResultController.php +++ b/app/Controllers/ResultController.php @@ -17,7 +17,7 @@ class ResultController extends Controller { /** * List results with optional filters - * GET /api/results + * GET /api/result */ public function index() { try { @@ -57,7 +57,7 @@ class ResultController extends Controller { /** * Get single result - * GET /api/results/{id} + * GET /api/result/{id} */ public function show($id) { try { @@ -89,7 +89,7 @@ class ResultController extends Controller { /** * Update result with validation - * PATCH /api/results/{id} + * PATCH /api/result/{id} */ public function update($id) { try { @@ -137,7 +137,7 @@ class ResultController extends Controller { /** * Soft delete result - * DELETE /api/results/{id} + * DELETE /api/result/{id} */ public function delete($id) { try { diff --git a/app/Controllers/Specimen/ContainerDefController.php b/app/Controllers/Specimen/ContainerDefController.php index d6daadf..5d081a6 100644 --- a/app/Controllers/Specimen/ContainerDefController.php +++ b/app/Controllers/Specimen/ContainerDefController.php @@ -73,11 +73,15 @@ class ContainerDefController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } - try { - $ConDefID = $this->model->update($input['ConDefID'], $input); + public function update($ConDefID = null) { + $input = $this->request->getJSON(true); + if (!$ConDefID || !ctype_digit((string) $ConDefID)) { + return $this->failValidationErrors('ConDefID is required.'); + } + $input['ConDefID'] = (int) $ConDefID; + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } + try { + $ConDefID = $this->model->update($input['ConDefID'], $input); return $this->respondCreated([ 'status' => 'success', 'message' => "data $ConDefID updated successfully" ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/Specimen/SpecimenCollectionController.php b/app/Controllers/Specimen/SpecimenCollectionController.php index dd3988e..917257c 100644 --- a/app/Controllers/Specimen/SpecimenCollectionController.php +++ b/app/Controllers/Specimen/SpecimenCollectionController.php @@ -66,11 +66,15 @@ class SpecimenCollectionController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } - try { - $id = $this->model->update($input['SpcColID'], $input); + public function update($SpcColID = null) { + $input = $this->request->getJSON(true); + if (!$SpcColID || !ctype_digit((string) $SpcColID)) { + return $this->failValidationErrors('SpcColID is required.'); + } + $input['SpcColID'] = (int) $SpcColID; + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } + try { + $id = $this->model->update($input['SpcColID'], $input); return $this->respondCreated([ 'status' => 'success', 'message' => "data $id updated successfully" ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/Specimen/SpecimenController.php b/app/Controllers/Specimen/SpecimenController.php index fe8df7e..7b3e1ca 100644 --- a/app/Controllers/Specimen/SpecimenController.php +++ b/app/Controllers/Specimen/SpecimenController.php @@ -66,11 +66,15 @@ class SpecimenController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } - try { - $id = $this->model->update($input['SID'], $input); + public function update($SID = null) { + $input = $this->request->getJSON(true); + if (!$SID || !ctype_digit((string) $SID)) { + return $this->failValidationErrors('SID is required.'); + } + $input['SID'] = (int) $SID; + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } + try { + $id = $this->model->update($input['SID'], $input); return $this->respondCreated([ 'status' => 'success', 'message' => "data $id updated successfully" ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/Specimen/SpecimenPrepController.php b/app/Controllers/Specimen/SpecimenPrepController.php index 0c4f4d6..2798cf3 100644 --- a/app/Controllers/Specimen/SpecimenPrepController.php +++ b/app/Controllers/Specimen/SpecimenPrepController.php @@ -51,11 +51,15 @@ class SpecimenPrepController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } - try { - $id = $this->model->update($input['SpcPrpID'], $input); + public function update($SpcPrpID = null) { + $input = $this->request->getJSON(true); + if (!$SpcPrpID || !ctype_digit((string) $SpcPrpID)) { + return $this->failValidationErrors('SpcPrpID is required.'); + } + $input['SpcPrpID'] = (int) $SpcPrpID; + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } + try { + $id = $this->model->update($input['SpcPrpID'], $input); return $this->respondCreated([ 'status' => 'success', 'message' => "data $id updated successfully" ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/Specimen/SpecimenStatusController.php b/app/Controllers/Specimen/SpecimenStatusController.php index 173385a..966b5c3 100644 --- a/app/Controllers/Specimen/SpecimenStatusController.php +++ b/app/Controllers/Specimen/SpecimenStatusController.php @@ -64,11 +64,15 @@ class ContainerDef extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } - try { - $id = $this->model->update($input['SpcStaID'], $input); + public function update($SpcStaID = null) { + $input = $this->request->getJSON(true); + if (!$SpcStaID || !ctype_digit((string) $SpcStaID)) { + return $this->failValidationErrors('SpcStaID is required.'); + } + $input['SpcStaID'] = (int) $SpcStaID; + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } + try { + $id = $this->model->update($input['SpcStaID'], $input); return $this->respondCreated([ 'status' => 'success', 'message' => "data $id updated successfully" ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/Test/TestMapController.php b/app/Controllers/Test/TestMapController.php index b600c35..7af0c8b 100644 --- a/app/Controllers/Test/TestMapController.php +++ b/app/Controllers/Test/TestMapController.php @@ -64,13 +64,17 @@ class TestMapController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - $id = $input["TestMapID"]; - if (!$id) { return $this->failValidationErrors('TestMapID is required.'); } - if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors() ); } - try { - $this->model->update($id,$input); + public function update($TestMapID = null) { + $input = $this->request->getJSON(true); + if (!$TestMapID || !ctype_digit((string) $TestMapID)) { return $this->failValidationErrors('TestMapID is required.'); } + $id = (int) $TestMapID; + if (isset($input['TestMapID']) && (string) $input['TestMapID'] !== (string) $id) { + return $this->failValidationErrors('TestMapID in URL does not match body.'); + } + $input['TestMapID'] = $id; + if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors() ); } + try { + $this->model->update($id,$input); return $this->respondCreated([ 'status' => 'success', 'message' => "data updated successfully", 'data' => $id ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/Test/TestMapDetailController.php b/app/Controllers/Test/TestMapDetailController.php index 85bd987..8a100a3 100644 --- a/app/Controllers/Test/TestMapDetailController.php +++ b/app/Controllers/Test/TestMapDetailController.php @@ -89,13 +89,16 @@ class TestMapDetailController extends BaseController { } } - public function update() { - $input = $this->request->getJSON(true); - $id = $input["TestMapDetailID"] ?? null; - - if (!$id) { - return $this->failValidationErrors('TestMapDetailID is required.'); - } + public function update($TestMapDetailID = null) { + $input = $this->request->getJSON(true); + if (!$TestMapDetailID || !ctype_digit((string) $TestMapDetailID)) { + return $this->failValidationErrors('TestMapDetailID is required.'); + } + $id = (int) $TestMapDetailID; + if (isset($input['TestMapDetailID']) && (string) $input['TestMapDetailID'] !== (string) $id) { + return $this->failValidationErrors('TestMapDetailID in URL does not match body.'); + } + $input['TestMapDetailID'] = $id; if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); diff --git a/app/Controllers/Test/TestsController.php b/app/Controllers/Test/TestsController.php index 939de8b..30a235d 100644 --- a/app/Controllers/Test/TestsController.php +++ b/app/Controllers/Test/TestsController.php @@ -169,18 +169,27 @@ class TestsController extends BaseController 'StartDate' => $input['StartDate'] ?? date('Y-m-d H:i:s'), ]; - $id = $this->model->insert($testSiteData); - if (!$id) { - throw new \Exception('Failed to insert main test definition'); - } - - $this->handleDetails($id, $input, 'insert'); + $id = $this->model->insert($testSiteData); + if (!$id) { + $dbError = $db->error(); + log_message('error', 'Test insert failed: ' . json_encode($dbError, JSON_UNESCAPED_SLASHES)); + $message = $dbError['message'] ?? 'Failed to insert main test definition'; + throw new \Exception('Failed to insert main test definition: ' . $message); + } + + $this->handleDetails($id, $input, 'insert'); $db->transComplete(); - if ($db->transStatus() === false) { - return $this->failServerError('Transaction failed'); - } + if ($db->transStatus() === false) { + $dbError = $db->error(); + $lastQuery = $db->showLastQuery(); + log_message('error', 'TestController transaction failed: ' . json_encode([ + 'error' => $dbError, + 'last_query' => $lastQuery, + ], JSON_UNESCAPED_SLASHES)); + return $this->failServerError('Transaction failed'); + } return $this->respondCreated([ 'status' => 'created', diff --git a/app/Controllers/User/UserController.php b/app/Controllers/User/UserController.php index 91be5f9..04cbb59 100644 --- a/app/Controllers/User/UserController.php +++ b/app/Controllers/User/UserController.php @@ -25,7 +25,7 @@ class UserController extends BaseController /** * List users with pagination and search - * GET /api/users?page=1&per_page=20&search=term + * GET /api/user?page=1&per_page=20&search=term */ public function index() { @@ -81,7 +81,7 @@ class UserController extends BaseController /** * Get single user by ID - * GET /api/users/(:num) + * GET /api/user/(:num) */ public function show($id) { @@ -116,7 +116,7 @@ class UserController extends BaseController /** * Create new user - * POST /api/users + * POST /api/user */ public function create() { @@ -173,14 +173,14 @@ class UserController extends BaseController /** * Update existing user - * PATCH /api/users + * PATCH /api/user/(:num) */ - public function update() + public function update($id) { try { $data = $this->request->getJSON(true); - if (empty($data['UserID'])) { + if (empty($id) || !ctype_digit((string) $id)) { return $this->respond([ 'status' => 'failed', 'message' => 'UserID is required', @@ -188,7 +188,15 @@ class UserController extends BaseController ], 400); } - $userId = $data['UserID']; + 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) @@ -243,7 +251,7 @@ class UserController extends BaseController /** * Delete user (soft delete) - * DELETE /api/users/(:num) + * DELETE /api/user/(:num) */ public function delete($id) { @@ -290,4 +298,4 @@ class UserController extends BaseController ], 500); } } -} \ No newline at end of file +} diff --git a/app/Libraries/TestValidationService.php b/app/Libraries/TestValidationService.php index ea57aea..dfd534f 100644 --- a/app/Libraries/TestValidationService.php +++ b/app/Libraries/TestValidationService.php @@ -168,13 +168,17 @@ class TestValidationService * @param string $refType * @return string|null Returns table name or null if no reference table needed */ - public static function getReferenceTable(string $resultType, string $refType): ?string - { - $resultType = strtoupper($resultType); - $refType = strtoupper($refType); - - return self::REFERENCE_TABLES[$resultType][$refType] ?? null; - } + public static function getReferenceTable(?string $resultType, ?string $refType): ?string + { + if ($resultType === null || $refType === null) { + return null; + } + + $resultType = strtoupper($resultType); + $refType = strtoupper($refType); + + return self::REFERENCE_TABLES[$resultType][$refType] ?? null; + } /** * Check if a test needs reference ranges @@ -182,11 +186,15 @@ class TestValidationService * @param string $resultType * @return bool */ - public static function needsReferenceRanges(string $resultType): bool - { - $resultType = strtoupper($resultType); - return $resultType !== 'NORES'; - } + public static function needsReferenceRanges(?string $resultType): bool + { + if ($resultType === null) { + return false; + } + + $resultType = strtoupper($resultType); + return $resultType !== 'NORES'; + } /** * Check if a test uses refnum table @@ -195,10 +203,10 @@ class TestValidationService * @param string $refType * @return bool */ - public static function usesRefNum(string $resultType, string $refType): bool - { - return self::getReferenceTable($resultType, $refType) === 'refnum'; - } + public static function usesRefNum(?string $resultType, ?string $refType): bool + { + return self::getReferenceTable($resultType, $refType) === 'refnum'; + } /** * Check if a test uses reftxt table @@ -207,10 +215,10 @@ class TestValidationService * @param string $refType * @return bool */ - public static function usesRefTxt(string $resultType, string $refType): bool - { - return self::getReferenceTable($resultType, $refType) === 'reftxt'; - } + public static function usesRefTxt(?string $resultType, ?string $refType): bool + { + return self::getReferenceTable($resultType, $refType) === 'reftxt'; + } /** * Check if TestType is CALC @@ -256,4 +264,4 @@ class TestValidationService $testType = strtoupper($testType); return in_array($testType, ['TEST', 'PARAM'], true); } -} \ No newline at end of file +} diff --git a/app/Models/Patient/PatientModel.php b/app/Models/Patient/PatientModel.php index 9a511dc..3753b9e 100644 --- a/app/Models/Patient/PatientModel.php +++ b/app/Models/Patient/PatientModel.php @@ -213,22 +213,23 @@ class PatientModel extends BaseModel { $db->transBegin(); try { - $InternalPID = $input['InternalPID']; - $previousData = $this->find($InternalPID); - $this->where('InternalPID',$InternalPID)->set($input)->update(); - $this->checkDbError($db, 'Update patient'); - - AuditService::logData( - 'UPDATE', - 'patient', - (string) $InternalPID, - 'patient', - null, - $previousData, - $input, - 'Patient data updated', - ['changed_fields' => array_keys(array_diff_assoc($previousData, $input))] - ); + $InternalPID = $input['InternalPID']; + $previousData = $this->find($InternalPID) ?? []; + $this->where('InternalPID',$InternalPID)->set($input)->update(); + $this->checkDbError($db, 'Update patient'); + + $changedFields = array_keys(array_diff_assoc((array) $previousData, (array) $input)); + AuditService::logData( + 'UPDATE', + 'patient', + (string) $InternalPID, + 'patient', + null, + (array) $previousData, + $input, + 'Patient data updated', + ['changed_fields' => $changedFields] + ); if (!empty($input['PatIdt'])) { $modelPatIdt->updatePatIdt($input['PatIdt'], $InternalPID); diff --git a/app/Models/RefRange/RefTxtModel.php b/app/Models/RefRange/RefTxtModel.php index 2f08e62..f3d0b42 100644 --- a/app/Models/RefRange/RefTxtModel.php +++ b/app/Models/RefRange/RefTxtModel.php @@ -90,18 +90,19 @@ class RefTxtModel extends BaseModel */ public function batchInsert($testSiteID, $siteID, $ranges) { - foreach ($ranges as $range) { - $this->insert([ - 'TestSiteID' => $testSiteID, - 'SiteID' => $siteID, - 'TxtRefType' => $range['TxtRefType'], - 'Sex' => $range['Sex'], - 'AgeStart' => (int) ($range['AgeStart'] ?? 0), - 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), - 'RefTxt' => $range['RefTxt'] ?? '', - 'Flag' => $range['Flag'] ?? null, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - } - } -} + foreach ($ranges as $range) { + $this->insert([ + 'TestSiteID' => $testSiteID, + 'SiteID' => $siteID, + 'SpcType' => $range['SpcType'] ?? 'GEN', + 'TxtRefType' => $range['TxtRefType'], + 'Sex' => $range['Sex'], + 'AgeStart' => (int) ($range['AgeStart'] ?? 0), + 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), + 'RefTxt' => $range['RefTxt'] ?? '', + 'Flag' => $range['Flag'] ?? null, + 'CreateDate' => date('Y-m-d H:i:s'), + ]); + } + } +} diff --git a/app/Models/Rule/RuleDefModel.php b/app/Models/Rule/RuleDefModel.php index 85e1ae5..73e9160 100644 --- a/app/Models/Rule/RuleDefModel.php +++ b/app/Models/Rule/RuleDefModel.php @@ -74,7 +74,7 @@ class RuleDefModel extends BaseModel ->get() ->getResultArray(); - return array_column($result, 'TestSiteID'); + return array_map('intval', array_column($result, 'TestSiteID')); } /** @@ -89,22 +89,24 @@ class RuleDefModel extends BaseModel $db = \Config\Database::connect(); // Check if already linked (and not soft deleted) - $existing = $db->table('testrule') - ->where('RuleID', $ruleID) - ->where('TestSiteID', $testSiteID) - ->where('EndDate IS NULL') - ->first(); + $existing = $db->table('testrule') + ->where('RuleID', $ruleID) + ->where('TestSiteID', $testSiteID) + ->where('EndDate IS NULL') + ->get() + ->getRowArray(); if ($existing) { return true; // Already linked } // Check if soft deleted - restore it - $softDeleted = $db->table('testrule') - ->where('RuleID', $ruleID) - ->where('TestSiteID', $testSiteID) - ->where('EndDate IS NOT NULL') - ->first(); + $softDeleted = $db->table('testrule') + ->where('RuleID', $ruleID) + ->where('TestSiteID', $testSiteID) + ->where('EndDate IS NOT NULL') + ->get() + ->getRowArray(); if ($softDeleted) { return $db->table('testrule') diff --git a/app/Services/AuditService.php b/app/Services/AuditService.php index fa46519..334b715 100644 --- a/app/Services/AuditService.php +++ b/app/Services/AuditService.php @@ -11,7 +11,7 @@ class AuditService { $this->db = \Config\Database::connect(); } - public static function logData( + public static function logData( string $operation, string $entityType, string $entityId, @@ -28,8 +28,8 @@ class AuditService { 'entity_id' => $entityId, 'table_name' => $tableName, 'field_name' => $fieldName, - 'previous_value' => $previousValue, - 'new_value' => $newValue, + 'previous_value' => self::normalizeAuditValue($previousValue), + 'new_value' => self::normalizeAuditValue($newValue), 'mechanism' => 'MANUAL', 'application_id' => 'CLQMS-WEB', 'web_page' => self::getUri(), @@ -41,7 +41,7 @@ class AuditService { 'ip_address' => self::getIpAddress(), 'user_id' => self::getUserId(), 'reason' => $reason, - 'context' => $context, + 'context' => self::normalizeAuditValue($context), 'created_at' => date('Y-m-d H:i:s') ]); } @@ -64,9 +64,9 @@ class AuditService { 'entity_id' => $entityId, 'service_class' => $serviceClass, 'resource_type' => $resourceType, - 'resource_details' => $resourceDetails, - 'previous_value' => $previousValue, - 'new_value' => $newValue, + 'resource_details' => self::normalizeAuditValue($resourceDetails), + 'previous_value' => self::normalizeAuditValue($previousValue), + 'new_value' => self::normalizeAuditValue($newValue), 'mechanism' => 'AUTOMATIC', 'application_id' => $serviceName ?? 'SYSTEM-SERVICE', 'service_name' => $serviceName, @@ -79,7 +79,7 @@ class AuditService { 'port' => $resourceDetails['port'] ?? null, 'user_id' => 'SYSTEM', 'reason' => null, - 'context' => $context, + 'context' => self::normalizeAuditValue($context), 'created_at' => date('Y-m-d H:i:s') ]); } @@ -102,8 +102,8 @@ class AuditService { 'entity_id' => $entityId, 'security_class' => $securityClass, 'resource_path' => $resourcePath, - 'previous_value' => $previousValue, - 'new_value' => $newValue, + 'previous_value' => self::normalizeAuditValue($previousValue), + 'new_value' => self::normalizeAuditValue($newValue), 'mechanism' => 'MANUAL', 'application_id' => 'CLQMS-WEB', 'web_page' => self::getUri(), @@ -115,7 +115,7 @@ class AuditService { 'ip_address' => self::getIpAddress(), 'user_id' => self::getUserId() ?? 'UNKNOWN', 'reason' => $reason, - 'context' => $context, + 'context' => self::normalizeAuditValue($context), 'created_at' => date('Y-m-d H:i:s') ]); } @@ -138,9 +138,9 @@ class AuditService { 'entity_id' => $entityId, 'error_code' => $errorCode, 'error_message' => $errorMessage, - 'error_details' => $errorDetails, - 'previous_value' => $previousValue, - 'new_value' => $newValue, + 'error_details' => self::normalizeAuditValue($errorDetails), + 'previous_value' => self::normalizeAuditValue($previousValue), + 'new_value' => self::normalizeAuditValue($newValue), 'mechanism' => 'AUTOMATIC', 'application_id' => 'CLQMS-WEB', 'web_page' => self::getUri(), @@ -152,15 +152,28 @@ class AuditService { 'ip_address' => self::getIpAddress(), 'user_id' => self::getUserId() ?? 'SYSTEM', 'reason' => $reason, - 'context' => $context, + 'context' => self::normalizeAuditValue($context), 'created_at' => date('Y-m-d H:i:s') ]); } - private static function log(string $table, array $data): void { - $db = \Config\Database::connect(); - $db->table($table)->insert($data); - } + private static function log(string $table, array $data): void { + $db = \Config\Database::connect(); + if (!$db->tableExists($table)) { + return; + } + $db->table($table)->insert($data); + } + + private static function normalizeAuditValue($value) + { + if ($value === null || is_scalar($value)) { + return $value; + } + + $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return $json !== false ? $json : null; + } private static function getUri(): ?string { return $_SERVER['REQUEST_URI'] ?? null; diff --git a/docs/test-rule-engine.md b/docs/test-rule-engine.md index e4b7921..f3e6d18 100644 --- a/docs/test-rule-engine.md +++ b/docs/test-rule-engine.md @@ -9,7 +9,7 @@ Rules are authored using a domain specific language stored in `ruledef.Condition ### Execution Flow 1. Write or edit the DSL in `ConditionExpr`. -2. POST the expression to `POST /api/rules/compile` to validate syntax and produce compiled JSON. +2. POST the expression to `POST /api/rule/compile` to validate syntax and produce compiled JSON. 3. Save the compiled payload into `ConditionExprCompiled` and persist the rule in `ruledef`. 4. Link the rule to one or more tests through `testrule.TestSiteID` (rules only run for linked tests). 5. When the configured event fires (`test_created` or `result_updated`), the engine evaluates `ConditionExprCompiled` and runs the resulting `then` or `else` actions. @@ -34,7 +34,7 @@ Rule └── Actions (what to do) ``` -The DSL expression lives in `ConditionExpr`. The compile endpoint (`/api/rules/compile`) renders the lifeblood of execution, producing `conditionExpr`, `valueExpr`, `then`, and `else` nodes that the engine consumes at runtime. +The DSL expression lives in `ConditionExpr`. The compile endpoint (`/api/rule/compile`) renders the lifeblood of execution, producing `conditionExpr`, `valueExpr`, `then`, and `else` nodes that the engine consumes at runtime. ## Syntax Guide @@ -148,7 +148,7 @@ if(requested('GLU'); test_delete('INS'):comment_insert('Duplicate insulin reques Validates the DSL and returns a compiled JSON structure that should be persisted in `ConditionExprCompiled`. ```http -POST /api/rules/compile +POST /api/rule/compile Content-Type: application/json { @@ -163,7 +163,7 @@ The response contains `raw`, `compiled`, and `conditionExprCompiled` fields; sto This endpoint simply evaluates an expression against a runtime context. It does not compile DSL or persist the result. ```http -POST /api/rules/validate +POST /api/rule/validate Content-Type: application/json { @@ -179,7 +179,7 @@ Content-Type: application/json ### Create Rule (example) ```http -POST /api/rules +POST /api/rule Content-Type: application/json { @@ -211,7 +211,7 @@ Content-Type: application/json ## Best Practices -1. Always run `POST /api/rules/compile` before persisting a rule so `ConditionExprCompiled` exists. +1. Always run `POST /api/rule/compile` before persisting a rule so `ConditionExprCompiled` exists. 2. Link each rule to the relevant tests via `testrule.TestSiteID`—rules are scoped to linked tests. 3. Use multi-action (`:`) to bundle several actions in a single branch; finish the branch with `nothing` if no further work is needed. 4. Prefer `comment_insert()` over the removed `set_priority()` action when documenting priority decisions. @@ -266,12 +266,12 @@ php spark migrate 2. Confirm the rule is linked to the relevant `TestSiteID` in `testrule`. 3. Verify the `EventCode` matches the currently triggered event (`test_created` or `result_updated`). 4. Check that `EndDate IS NULL` for both `ruledef` and `testrule` (soft deletes disable execution). -5. Use `/api/rules/compile` to validate the DSL and view errors. +5. Use `/api/rule/compile` to validate the DSL and view errors. ### Invalid Expression -1. POST the expression to `/api/rules/compile` to get a detailed compilation error. -2. If using `/api/rules/validate`, supply the expected `context` payload; the endpoint simply evaluates the expression without saving it. +1. POST the expression to `/api/rule/compile` to get a detailed compilation error. +2. If using `/api/rule/validate`, supply the expected `context` payload; the endpoint simply evaluates the expression without saving it. ### Runtime Errors diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8fdc73c..7489eac 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - - + + @@ -56,7 +56,7 @@ - + diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index e20f646..7c4e778 100644 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -23,40 +23,40 @@ servers: tags: - name: Authentication description: User authentication and session management - - name: Patients + - name: Patient description: Patient registration and management - - name: Patient Visits + - 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: Tests + - name: Test description: Test definitions and test catalog - - name: Calculations + - 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: Orders + - name: Order description: Laboratory order management - - name: Results + - name: Result description: Patient results reporting with auto-validation - - name: Reports + - name: Report description: Lab report generation (HTML view) - name: Edge API description: Instrument integration endpoints - - name: Contacts + - name: Contact description: Contact management (doctors, practitioners, etc.) - - name: Locations - description: Location management (rooms, wards, buildings) - - name: ValueSets + - name: ValueSet description: Value set definitions and items + - name: User + description: User management and administration - name: Demo description: Demo/test endpoints (no authentication) - - name: EquipmentList - description: Laboratory equipment and instrument management - - name: Users - description: User management and administration - - name: Rules - description: Rule engine - rules can be linked to multiple tests via testrule mapping table paths: /api/auth/login: post: @@ -229,7 +229,7 @@ paths: /api/calc/{codeOrName}: post: tags: - - Calculations + - Calculation summary: Evaluate a configured calculation by test code or name and return the numeric result only. security: [] parameters: @@ -267,7 +267,7 @@ paths: /api/contact: get: tags: - - Contacts + - Contact summary: List contacts security: - bearerAuth: [] @@ -300,7 +300,7 @@ paths: $ref: '#/components/schemas/Contact' post: tags: - - Contacts + - Contact summary: Create new contact security: - bearerAuth: [] @@ -365,10 +365,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - patch: + delete: tags: - - Contacts - summary: Update contact + - Contact + summary: Delete contact security: - bearerAuth: [] requestBody: @@ -379,11 +379,67 @@ paths: type: object required: - ContactID - - NameFirst properties: ContactID: type: integer - description: Contact ID to update + 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 @@ -436,59 +492,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Contacts - 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: - - Contacts - 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' /api/demo/hello: get: tags: @@ -532,7 +535,7 @@ paths: timestamp: type: string format: date-time - /api/edge/results: + /api/edge/result: post: tags: - Edge API @@ -555,7 +558,7 @@ paths: $ref: '#/components/schemas/EdgeResultResponse' '400': description: Invalid JSON payload - /api/edge/orders: + /api/edge/order: get: tags: - Edge API @@ -589,7 +592,7 @@ paths: type: array items: $ref: '#/components/schemas/EdgeOrder' - /api/edge/orders/{orderId}/ack: + /api/edge/order/{orderId}/ack: post: tags: - Edge API @@ -645,7 +648,7 @@ paths: /api/equipmentlist: get: tags: - - EquipmentList + - Equipment summary: List equipment description: Get list of equipment with optional filters security: @@ -697,7 +700,7 @@ paths: $ref: '#/components/schemas/EquipmentList' post: tags: - - EquipmentList + - Equipment summary: Create equipment description: Create a new equipment entry security: @@ -749,11 +752,11 @@ paths: type: string data: type: integer - patch: + delete: tags: - - EquipmentList - summary: Update equipment - description: Update an existing equipment entry + - Equipment + summary: Delete equipment + description: Soft delete an equipment entry security: - bearerAuth: [] requestBody: @@ -767,6 +770,68 @@ paths: 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 @@ -802,69 +867,10 @@ paths: type: string data: type: integer - delete: - tags: - - EquipmentList - 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: - - EquipmentList - 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' /api/location: get: tags: - - Locations + - Location summary: List locations security: - bearerAuth: [] @@ -897,7 +903,7 @@ paths: $ref: '#/components/schemas/Location' post: tags: - - Locations + - Location summary: Create location security: - bearerAuth: [] @@ -946,10 +952,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - patch: + delete: tags: - - Locations - summary: Update location + - Location + summary: Delete location security: - bearerAuth: [] requestBody: @@ -963,7 +969,62 @@ paths: properties: LocationID: type: integer - description: Location ID to update + 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 @@ -999,63 +1060,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Locations - 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: - - Locations - 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' /api/ordertest: get: tags: - - Orders + - Order summary: List orders security: - bearerAuth: [] @@ -1109,7 +1117,7 @@ paths: $ref: '#/components/schemas/OrderTestList' post: tags: - - Orders + - 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: @@ -1186,61 +1194,9 @@ paths: description: Validation error '500': description: Server error - patch: - tags: - - Orders - summary: Update order - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - OrderID - properties: - OrderID: - type: string - 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' delete: tags: - - Orders + - Order summary: Delete order security: - bearerAuth: [] @@ -1261,7 +1217,7 @@ paths: /api/ordertest/status: post: tags: - - Orders + - Order summary: Update order status security: - bearerAuth: [] @@ -1310,7 +1266,7 @@ paths: /api/ordertest/{id}: get: tags: - - Orders + - Order summary: Get order by ID description: Returns order details with associated specimens and tests security: @@ -1336,6 +1292,61 @@ paths: 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/{id}: get: tags: @@ -1381,32 +1392,6 @@ paths: responses: '201': description: Site created - patch: - tags: - - Organization - summary: Update site - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - SiteName: - type: string - SiteCode: - type: string - AccountID: - type: integer - responses: - '200': - description: Site updated delete: tags: - Organization @@ -1443,6 +1428,34 @@ paths: 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: @@ -1468,36 +1481,6 @@ paths: responses: '201': description: Discipline created - patch: - tags: - - Organization - summary: Update discipline - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - 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 - responses: - '200': - description: Discipline updated delete: tags: - Organization @@ -1534,6 +1517,38 @@ paths: 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: @@ -1559,32 +1574,6 @@ paths: responses: '201': description: Department created - patch: - tags: - - Organization - summary: Update department - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - DeptName: - type: string - DeptCode: - type: string - SiteID: - type: integer - responses: - '200': - description: Department updated delete: tags: - Organization @@ -1621,6 +1610,34 @@ paths: 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: @@ -1646,34 +1663,6 @@ paths: responses: '201': description: Workstation created - patch: - tags: - - Organization - summary: Update workstation - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - WorkstationName: - type: string - WorkstationCode: - type: string - SiteID: - type: integer - DepartmentID: - type: integer - responses: - '200': - description: Workstation updated delete: tags: - Organization @@ -1710,6 +1699,36 @@ paths: 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: @@ -1757,30 +1776,6 @@ paths: responses: '201': description: Host application created - patch: - tags: - - Organization - summary: Update host application - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - HostAppName: - type: string - SiteID: - type: integer - responses: - '200': - description: Host application updated delete: tags: - Organization @@ -1821,6 +1816,32 @@ paths: 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: @@ -1868,32 +1889,6 @@ paths: responses: '201': description: Host communication parameters created - patch: - tags: - - Organization - summary: Update host communication parameters - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - HostIP: - type: string - HostPort: - type: string - HostPwd: - type: string - responses: - '200': - description: Host communication parameters updated delete: tags: - Organization @@ -1934,6 +1929,34 @@ paths: 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: @@ -1981,32 +2004,6 @@ paths: responses: '201': description: Coding system created - patch: - tags: - - Organization - summary: Update coding system - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - CodingSysID - properties: - CodingSysID: - type: integer - CodingSysAbb: - type: string - FullText: - type: string - Description: - type: string - responses: - '200': - description: Coding system updated delete: tags: - Organization @@ -2047,10 +2044,38 @@ paths: 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 Visits + - Patient Visit summary: List patient visits security: - bearerAuth: [] @@ -2122,7 +2147,7 @@ paths: description: Number of records per page post: tags: - - Patient Visits + - Patient Visit summary: Create patient visit description: | Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided. @@ -2199,27 +2224,66 @@ paths: 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 Visits + - 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 - required: - - InternalPVID properties: - InternalPVID: - type: integer - description: Visit ID (required) PVID: type: string InternalPID: @@ -2282,47 +2346,10 @@ paths: type: string InternalPVID: type: integer - delete: - tags: - - Patient Visits - summary: Delete patient visit - security: - - bearerAuth: [] - responses: - '200': - description: Visit deleted successfully - /api/patvisit/{id}: - get: - tags: - - Patient Visits - 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' /api/patvisit/patient/{patientId}: get: tags: - - Patient Visits + - Patient Visit summary: Get visits by patient ID security: - bearerAuth: [] @@ -2350,7 +2377,7 @@ paths: /api/patvisitadt: post: tags: - - Patient Visits + - Patient Visit summary: Create ADT record description: Create a new Admission/Discharge/Transfer record security: @@ -2368,29 +2395,9 @@ paths: application/json: schema: $ref: '#/components/schemas/SuccessResponse' - patch: - tags: - - Patient Visits - summary: Update ADT record - description: Update an existing ADT record - security: - - bearerAuth: [] - 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' delete: tags: - - Patient Visits + - Patient Visit summary: Delete ADT visit (soft delete) security: - bearerAuth: [] @@ -2412,7 +2419,7 @@ paths: /api/patvisitadt/visit/{visitId}: get: tags: - - Patient Visits + - 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: @@ -2489,31 +2496,10 @@ paths: EndDate: type: string format: date-time - delete: - tags: - - Patient Visits - 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/{id}: get: tags: - - Patient Visits + - Patient Visit summary: Get ADT record by ID description: Retrieve a single ADT record by its ID, including location and doctor details security: @@ -2588,10 +2574,37 @@ paths: 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: - - Patients + - Patient summary: List patients security: - bearerAuth: [] @@ -2636,7 +2649,7 @@ paths: $ref: '#/components/schemas/PatientListResponse' post: tags: - - Patients + - Patient summary: Create new patient security: - bearerAuth: [] @@ -2659,24 +2672,9 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - patch: - tags: - - Patients - summary: Update patient - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Patient' - responses: - '200': - description: Patient updated successfully delete: tags: - - Patients + - Patient summary: Delete patient (soft delete) security: - bearerAuth: [] @@ -2698,7 +2696,7 @@ paths: /api/patient/check: get: tags: - - Patients + - Patient summary: Check if patient exists security: - bearerAuth: [] @@ -2729,7 +2727,7 @@ paths: /api/patient/{id}: get: tags: - - Patients + - Patient summary: Get patient by ID security: - bearerAuth: [] @@ -2752,10 +2750,32 @@ paths: type: string data: $ref: '#/components/schemas/Patient' - /api/reports/{orderID}: + patch: + tags: + - Patient + summary: 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/Patient' + responses: + '200': + description: Patient updated successfully + /api/report/{orderID}: get: tags: - - Reports + - 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: @@ -2787,10 +2807,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - /api/results: + /api/result: get: tags: - - Results + - Result summary: List results description: Retrieve patient test results with optional filters by order or patient security: @@ -2883,10 +2903,10 @@ paths: RefDisplay: type: string nullable: true - /api/results/{id}: + /api/result/{id}: get: tags: - - Results + - Result summary: Get result by ID description: Retrieve a specific result entry with all related data security: @@ -2993,7 +3013,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' patch: tags: - - Results + - 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: @@ -3066,7 +3086,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' delete: tags: - - Results + - Result summary: Delete result description: Soft delete a result entry by setting DelDate security: @@ -3091,10 +3111,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - /api/rules: + /api/rule: get: tags: - - Rules + - Rule summary: List rules security: - bearerAuth: [] @@ -3132,7 +3152,7 @@ paths: $ref: '#/components/schemas/RuleDef' post: tags: - - Rules + - Rule summary: Create rule description: | Create a new rule. Rules must be linked to at least one test via TestSiteIDs. @@ -3174,7 +3194,7 @@ paths: ConditionExprCompiled: type: string nullable: true - description: Compiled JSON payload from POST /api/rules/compile + description: Compiled JSON payload from POST /api/rule/compile required: - RuleCode - RuleName @@ -3183,10 +3203,10 @@ paths: responses: '201': description: Rule created - /api/rules/{id}: + /api/rule/{id}: get: tags: - - Rules + - Rule summary: Get rule with linked tests security: - bearerAuth: [] @@ -3215,7 +3235,7 @@ paths: description: Rule not found patch: tags: - - Rules + - Rule summary: Update rule description: | Update a rule. TestSiteIDs can be provided to update which tests the rule is linked to. @@ -3260,7 +3280,7 @@ paths: description: Rule not found delete: tags: - - Rules + - Rule summary: Soft delete rule security: - bearerAuth: [] @@ -3276,10 +3296,10 @@ paths: description: Rule deleted '404': description: Rule not found - /api/rules/validate: + /api/rule/validate: post: tags: - - Rules + - Rule summary: Validate/evaluate an expression security: - bearerAuth: [] @@ -3300,10 +3320,10 @@ paths: responses: '200': description: Validation result - /api/rules/compile: + /api/rule/compile: post: tags: - - Rules + - Rule summary: Compile DSL expression to engine-compatible structure description: | Compile a DSL expression to the engine-compatible JSON structure. @@ -3374,21 +3394,6 @@ paths: responses: '201': description: Specimen created - patch: - tags: - - Specimen - summary: Update specimen - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Specimen' - responses: - '200': - description: Specimen updated /api/specimen/{id}: get: tags: @@ -3405,6 +3410,28 @@ paths: 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 @@ -3492,15 +3519,6 @@ paths: responses: '201': description: Container definition created - patch: - tags: - - Specimen - summary: Update container definition - security: - - bearerAuth: [] - responses: - '200': - description: Container definition updated /api/specimen/container/{id}: get: tags: @@ -3517,6 +3535,28 @@ paths: 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: @@ -3542,12 +3582,26 @@ paths: 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 @@ -3576,15 +3630,6 @@ paths: responses: '201': description: Specimen preparation created - patch: - tags: - - Specimen - summary: Update specimen preparation - security: - - bearerAuth: [] - responses: - '200': - description: Specimen preparation updated /api/specimen/prep/{id}: get: tags: @@ -3601,6 +3646,28 @@ paths: 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: @@ -3626,15 +3693,6 @@ paths: responses: '201': description: Specimen status created - patch: - tags: - - Specimen - summary: Update specimen status - security: - - bearerAuth: [] - responses: - '200': - description: Specimen status updated /api/specimen/status/{id}: get: tags: @@ -3651,6 +3709,28 @@ paths: 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: @@ -3676,15 +3756,6 @@ paths: responses: '201': description: Collection method created - patch: - tags: - - Specimen - summary: Update specimen collection method - security: - - bearerAuth: [] - responses: - '200': - description: Collection method updated /api/specimen/collection/{id}: get: tags: @@ -3701,10 +3772,32 @@ paths: 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: - - Tests + - Test summary: List all test mappings security: - bearerAuth: [] @@ -3742,7 +3835,7 @@ paths: type: string post: tags: - - Tests + - Test summary: Create test mapping (header only) security: - bearerAuth: [] @@ -3802,54 +3895,9 @@ paths: data: type: integer description: Created TestMapID - patch: - tags: - - Tests - summary: Update test mapping - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapID: - type: integer - description: Test Map ID (required) - TestCode: - type: string - description: Test Code - maps to HostTestCode or ClientTestCode - HostType: - type: string - HostID: - type: string - ClientType: - type: string - ClientID: - type: string - required: - - TestMapID - 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 delete: tags: - - Tests + - Test summary: Soft delete test mapping (cascades to details) security: - bearerAuth: [] @@ -3886,7 +3934,7 @@ paths: /api/test/testmap/{id}: get: tags: - - Tests + - Test summary: Get test mapping by ID with details security: - bearerAuth: [] @@ -3913,10 +3961,57 @@ paths: $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: + TestCode: + type: string + description: Test Code - maps to HostTestCode or ClientTestCode + HostType: + type: string + HostID: + type: string + ClientType: + type: string + ClientID: + 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: - - Tests + - Test summary: Get test mappings by test code with details security: - bearerAuth: [] @@ -3946,7 +4041,7 @@ paths: /api/test/testmap/detail: get: tags: - - Tests + - Test summary: List test mapping details security: - bearerAuth: [] @@ -3974,7 +4069,7 @@ paths: $ref: '#/components/schemas/TestMapDetail' post: tags: - - Tests + - Test summary: Create test mapping detail security: - bearerAuth: [] @@ -4015,42 +4110,9 @@ paths: data: type: integer description: Created TestMapDetailID - patch: - tags: - - Tests - summary: Update test mapping detail - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapDetailID: - type: integer - description: Test Map Detail ID (required) - TestMapID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - required: - - TestMapDetailID - responses: - '200': - description: Test mapping detail updated delete: tags: - - Tests + - Test summary: Soft delete test mapping detail security: - bearerAuth: [] @@ -4072,7 +4134,7 @@ paths: /api/test/testmap/detail/{id}: get: tags: - - Tests + - Test summary: Get test mapping detail by ID security: - bearerAuth: [] @@ -4097,10 +4159,45 @@ paths: 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: - - Tests + - Test summary: Get test mapping details by test map ID security: - bearerAuth: [] @@ -4130,7 +4227,7 @@ paths: /api/test/testmap/detail/batch: post: tags: - - Tests + - Test summary: Batch create test mapping details security: - bearerAuth: [] @@ -4160,7 +4257,7 @@ paths: description: Batch create results patch: tags: - - Tests + - Test summary: Batch update test mapping details security: - bearerAuth: [] @@ -4192,7 +4289,7 @@ paths: description: Batch update results delete: tags: - - Tests + - Test summary: Batch delete test mapping details security: - bearerAuth: [] @@ -4211,7 +4308,7 @@ paths: /api/test: get: tags: - - Tests + - Test summary: List test definitions security: - bearerAuth: [] @@ -4287,7 +4384,7 @@ paths: description: Total number of records matching the query post: tags: - - Tests + - Test summary: Create test definition security: - bearerAuth: [] @@ -4450,29 +4547,462 @@ paths: - TestSiteName - TestType examples: - CALC_test: - summary: Create calculated test with members + TEST_no_ref: + summary: Technical test without reference or map value: SiteID: 1 - TestSiteCode: IBIL - TestSiteName: Indirect Bilirubin + TestSiteCode: TEST_NREF + TestSiteName: Numeric Test + TestType: TEST + SeqScr: 500 + SeqRpt: 500 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + 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 + VisibleScr: 1 + VisibleRpt: 0 + CountStat: 0 + details: + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 18 + AgeEnd: 99 + Flag: 'N' + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 18 + AgeEnd: 99 + Flag: 'N' + - NumRefType: NMRC + RangeType: REF + Sex: '1' + LowSign: '>' + Low: 75 + HighSign: < + High: 105 + AgeStart: 18 + AgeEnd: 99 + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '1' + details: + - HostTestCode: GLU + HostTestName: Glucose + ConDefID: 1 + ClientTestCode: GLU_C + ClientTestName: Glucose Client + - HostTestCode: CREA + HostTestName: Creatinine + ConDefID: 2 + ClientTestCode: CREA_C + ClientTestName: Creatinine Client + - HostType: WST + HostID: '3' + ClientType: INST + ClientID: '2' + details: + - HostTestCode: HB + HostTestName: Hemoglobin + ConDefID: 3 + ClientTestCode: HB_C + ClientTestName: Hemoglobin Client + TEST_threshold: + summary: Technical test with threshold reference + value: + SiteID: 1 + TestSiteCode: TEST_THLD + TestSiteName: Sodium Threshold + TestType: TEST + SeqScr: 115 + SeqRpt: 115 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 125 + Flag: H + TEST_threshold_map: + summary: Threshold reference plus test map + value: + SiteID: 1 + TestSiteCode: TEST_TMAP + TestSiteName: Potassium Panic + TestType: TEST + SeqScr: 120 + SeqRpt: 120 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 125 + Flag: H + - NumRefType: THOLD + RangeType: PANIC + Sex: '1' + LowSign: < + Low: 121 + AgeStart: 0 + AgeEnd: 125 + 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 + TEST_text: + summary: Technical test with text reference + value: + SiteID: 1 + TestSiteCode: TEST_TEXT + TestSiteName: Disease Stage + TestType: TEST + SeqScr: 130 + SeqRpt: 130 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + Method: Morphology + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 18 + AgeEnd: 99 + RefTxt: NORM=Normal;HIGH=High + Flag: 'N' + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 18 + AgeEnd: 99 + RefTxt: NORM=Normal + Flag: 'N' + - SpcType: GEN + TxtRefType: TEXT + Sex: '1' + AgeStart: 18 + AgeEnd: 99 + 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 + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + Method: Visual + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 120 + RefTxt: NORM=Normal;MACRO=Macro + Flag: 'N' + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 120 + 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 + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '8' + details: + - HostTestCode: UGLUC + HostTestName: Urine Glucose + ConDefID: 12 + ClientTestCode: UGLUC_C + ClientTestName: Urine Glucose Client + CALC_basic: + summary: Calculated test with members (no references) + value: + SiteID: 1 + TestSiteCode: CALC_BASE + TestSiteName: Estimated GFR TestType: CALC - Description: Bilirubin Indirek - SeqScr: 210 - SeqRpt: 210 + SeqScr: 190 + SeqRpt: 190 VisibleScr: 1 VisibleRpt: 1 CountStat: 0 details: DisciplineID: 2 DepartmentID: 2 - FormulaCode: '{TBIL} - {DBIL}' - RefType: RANGE - Unit1: mg/dL - Decimal: 2 + FormulaCode: CKD_EPI(CREA,AGE,GENDER) members: + - TestSiteID: 21 - TestSiteID: 22 - - TestSiteID: 23 + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 + details: + DisciplineID: 2 + DepartmentID: 2 + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + members: + - TestSiteID: 21 + - TestSiteID: 22 + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 10 + HighSign: LE + High: 20 + AgeStart: 18 + AgeEnd: 120 + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: EGFR + HostTestName: eGFR + ConDefID: 1 + ClientTestCode: EGFR_C + ClientTestName: eGFR Client + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + members: + - TestSiteID: 169 + - TestSiteID: 170 + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: LIPID + HostTestName: Lipid Profile + ConDefID: 1 + ClientTestCode: LIPID_C + ClientTestName: Lipid Client responses: '201': description: Test definition created @@ -4506,7 +5036,7 @@ paths: example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' patch: tags: - - Tests + - Test summary: Update test definition security: - bearerAuth: [] @@ -4696,7 +5226,7 @@ paths: /api/test/{id}: get: tags: - - Tests + - Test summary: Get test definition by ID security: - bearerAuth: [] @@ -4725,7 +5255,7 @@ paths: description: Test not found delete: tags: - - Tests + - Test summary: Soft delete test definition security: - bearerAuth: [] @@ -4770,10 +5300,10 @@ paths: description: Test not found '422': description: Test already disabled - /api/users: + /api/user: get: tags: - - Users + - User summary: List users with pagination and search security: - bearerAuth: [] @@ -4831,7 +5361,7 @@ paths: description: Server error post: tags: - - Users + - User summary: Create new user security: - bearerAuth: [] @@ -4881,12 +5411,44 @@ paths: 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: - - Users + - User summary: Update existing user security: - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID requestBody: required: true content: @@ -4922,34 +5484,9 @@ paths: description: User not found '500': description: Server error - /api/users/{id}: - get: - tags: - - Users - 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 delete: tags: - - Users + - User summary: Delete user (soft delete) security: - bearerAuth: [] @@ -4986,7 +5523,7 @@ paths: /api/valueset: get: tags: - - ValueSets + - 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: @@ -5025,7 +5562,7 @@ paths: /api/valueset/{key}: get: tags: - - ValueSets + - ValueSet summary: Get lib value set by key description: | Get a specific library/system value set from JSON files. @@ -5152,7 +5689,7 @@ paths: /api/valueset/refresh: post: tags: - - ValueSets + - 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: @@ -5174,7 +5711,7 @@ paths: /api/valueset/user/items: get: tags: - - ValueSets + - ValueSet summary: List user value set items description: List value set items from database (user-defined) security: @@ -5211,7 +5748,7 @@ paths: $ref: '#/components/schemas/ValueSetItem' post: tags: - - ValueSets + - ValueSet summary: Create user value set item description: Create value set item in database (user-defined) security: @@ -5257,7 +5794,7 @@ paths: /api/valueset/user/items/{id}: get: tags: - - ValueSets + - ValueSet summary: Get user value set item by ID description: Get value set item from database (user-defined) security: @@ -5282,7 +5819,7 @@ paths: $ref: '#/components/schemas/ValueSetItem' put: tags: - - ValueSets + - ValueSet summary: Update user value set item description: Update value set item in database (user-defined) security: @@ -5331,7 +5868,7 @@ paths: $ref: '#/components/schemas/ValueSetItem' delete: tags: - - ValueSets + - ValueSet summary: Delete user value set item description: Delete value set item from database (user-defined) security: @@ -5357,7 +5894,7 @@ paths: /api/valueset/user/def: get: tags: - - ValueSets + - ValueSet summary: List user value set definitions description: List value set definitions from database (user-defined) security: @@ -5405,7 +5942,7 @@ paths: type: integer post: tags: - - ValueSets + - ValueSet summary: Create user value set definition description: Create value set definition in database (user-defined) security: @@ -5443,7 +5980,7 @@ paths: /api/valueset/user/def/{id}: get: tags: - - ValueSets + - ValueSet summary: Get user value set definition by ID description: Get value set definition from database (user-defined) security: @@ -5468,7 +6005,7 @@ paths: $ref: '#/components/schemas/ValueSetDef' put: tags: - - ValueSets + - ValueSet summary: Update user value set definition description: Update value set definition in database (user-defined) security: @@ -5511,7 +6048,7 @@ paths: $ref: '#/components/schemas/ValueSetDef' delete: tags: - - ValueSets + - ValueSet summary: Delete user value set definition description: Delete value set definition from database (user-defined) security: diff --git a/public/api-docs.yaml b/public/api-docs.yaml index b431273..6b6ce5a 100644 --- a/public/api-docs.yaml +++ b/public/api-docs.yaml @@ -22,43 +22,43 @@ servers: - url: https://clqms01-api.services-summit.my.id/ description: Production server -tags: - - name: Authentication - description: User authentication and session management - - name: Patients - description: Patient registration and management - - name: Patient Visits - description: Patient visit/encounter management - - name: Organization - description: Organization structure (accounts, sites, disciplines, departments, workstations) - - name: Specimen - description: Specimen and container management - - name: Tests - description: Test definitions and test catalog - - name: Calculations - description: Lightweight calculator endpoint for retrieving computed values by code or name - - name: Orders - description: Laboratory order management - - name: Results - description: Patient results reporting with auto-validation - - name: Reports - description: Lab report generation (HTML view) - - name: Edge API - description: Instrument integration endpoints - - name: Contacts - description: Contact management (doctors, practitioners, etc.) - - name: Locations - description: Location management (rooms, wards, buildings) - - name: ValueSets - description: Value set definitions and items - - name: Demo - description: Demo/test endpoints (no authentication) - - name: EquipmentList - description: Laboratory equipment and instrument management - - name: Users - description: User management and administration - - name: Rules - description: Rule engine - rules can be linked to multiple tests via testrule mapping table +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) components: securitySchemes: diff --git a/public/paths/calc.yaml b/public/paths/calc.yaml index e2cb839..29681e2 100644 --- a/public/paths/calc.yaml +++ b/public/paths/calc.yaml @@ -1,6 +1,6 @@ /api/calc/{codeOrName}: - post: - tags: [Calculations] + post: + tags: [Calculation] summary: Evaluate a configured calculation by test code or name and return the numeric result only. security: [] parameters: diff --git a/public/paths/contact.yaml b/public/paths/contact.yaml index df1b341..f3e5348 100644 --- a/public/paths/contact.yaml +++ b/public/paths/contact.yaml @@ -1,6 +1,6 @@ /api/contact: - get: - tags: [Contacts] + get: + tags: [Contact] summary: List contacts security: - bearerAuth: [] @@ -32,8 +32,8 @@ items: $ref: '../components/schemas/master-data.yaml#/Contact' - post: - tags: [Contacts] + post: + tags: [Contact] summary: Create new contact security: - bearerAuth: [] @@ -99,79 +99,9 @@ schema: $ref: '../components/schemas/common.yaml#/ErrorResponse' - patch: - tags: [Contacts] - summary: Update contact - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - ContactID - - NameFirst - properties: - ContactID: - type: integer - description: Contact ID to update - 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 updated successfully - content: - application/json: - schema: - $ref: '../components/schemas/common.yaml#/SuccessResponse' - '422': - description: Validation error - content: - application/json: - schema: - $ref: '../components/schemas/common.yaml#/ErrorResponse' - delete: - tags: [Contacts] + delete: + tags: [Contact] summary: Delete contact security: - bearerAuth: [] @@ -195,9 +125,9 @@ schema: $ref: '../components/schemas/common.yaml#/SuccessResponse' -/api/contact/{id}: - get: - tags: [Contacts] +/api/contact/{id}: + get: + tags: [Contact] summary: Get contact by ID security: - bearerAuth: [] @@ -208,17 +138,91 @@ 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/master-data.yaml#/Contact' + responses: + '200': + description: Contact details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/master-data.yaml#/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 + responses: + '201': + description: Contact updated successfully + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/SuccessResponse' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/ErrorResponse' diff --git a/public/paths/edge-api.yaml b/public/paths/edge-api.yaml index ae397b9..c83fed6 100644 --- a/public/paths/edge-api.yaml +++ b/public/paths/edge-api.yaml @@ -1,4 +1,4 @@ -/api/edge/results: +/api/edge/result: post: tags: [Edge API] summary: Receive results from instrument (tiny-edge) @@ -21,7 +21,7 @@ '400': description: Invalid JSON payload -/api/edge/orders: +/api/edge/order: get: tags: [Edge API] summary: Fetch pending orders for instruments @@ -53,7 +53,7 @@ items: $ref: '../components/schemas/edge-api.yaml#/EdgeOrder' -/api/edge/orders/{orderId}/ack: +/api/edge/order/{orderId}/ack: post: tags: [Edge API] summary: Acknowledge order delivery diff --git a/public/paths/equipmentlist.yaml b/public/paths/equipmentlist.yaml index ae9fadb..040085c 100644 --- a/public/paths/equipmentlist.yaml +++ b/public/paths/equipmentlist.yaml @@ -1,6 +1,6 @@ /api/equipmentlist: - get: - tags: [EquipmentList] + get: + tags: [Equipment] summary: List equipment description: Get list of equipment with optional filters security: @@ -49,8 +49,8 @@ items: $ref: '../components/schemas/equipmentlist.yaml#/EquipmentList' - post: - tags: [EquipmentList] + post: + tags: [Equipment] summary: Create equipment description: Create a new equipment entry security: @@ -101,59 +101,9 @@ data: type: integer - patch: - tags: [EquipmentList] - summary: Update equipment - description: Update an existing equipment entry - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - EID - properties: - EID: - type: integer - 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 - delete: - tags: [EquipmentList] + delete: + tags: [Equipment] summary: Delete equipment description: Soft delete an equipment entry security: @@ -182,9 +132,9 @@ message: type: string -/api/equipmentlist/{id}: - get: - tags: [EquipmentList] +/api/equipmentlist/{id}: + get: + tags: [Equipment] summary: Get equipment by ID description: Get a single equipment entry by its EID security: @@ -196,17 +146,71 @@ 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.yaml#/EquipmentList' + responses: + '200': + description: Equipment details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/equipmentlist.yaml#/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 diff --git a/public/paths/locations.yaml b/public/paths/locations.yaml index 52d61a6..2653749 100644 --- a/public/paths/locations.yaml +++ b/public/paths/locations.yaml @@ -1,6 +1,6 @@ /api/location: - get: - tags: [Locations] + get: + tags: [Location] summary: List locations security: - bearerAuth: [] @@ -32,8 +32,8 @@ items: $ref: '../components/schemas/master-data.yaml#/Location' - post: - tags: [Locations] + post: + tags: [Location] summary: Create location security: - bearerAuth: [] @@ -83,61 +83,9 @@ schema: $ref: '../components/schemas/common.yaml#/ErrorResponse' - patch: - tags: [Locations] - summary: Update location - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - LocationID - properties: - LocationID: - type: integer - description: Location ID to update - 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/common.yaml#/SuccessResponse' - '422': - description: Validation error - content: - application/json: - schema: - $ref: '../components/schemas/common.yaml#/ErrorResponse' - delete: - tags: [Locations] + delete: + tags: [Location] summary: Delete location security: - bearerAuth: [] @@ -161,9 +109,9 @@ schema: $ref: '../components/schemas/common.yaml#/SuccessResponse' -/api/location/{id}: - get: - tags: [Locations] +/api/location/{id}: + get: + tags: [Location] summary: Get location by ID security: - bearerAuth: [] @@ -174,17 +122,72 @@ 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/master-data.yaml#/Location' + responses: + '200': + description: Location details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/master-data.yaml#/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/common.yaml#/SuccessResponse' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/ErrorResponse' diff --git a/public/paths/orders.yaml b/public/paths/orders.yaml index d84089a..74b17ec 100644 --- a/public/paths/orders.yaml +++ b/public/paths/orders.yaml @@ -1,6 +1,6 @@ /api/ordertest: - get: - tags: [Orders] + get: + tags: [Order] summary: List orders security: - bearerAuth: [] @@ -47,8 +47,8 @@ items: $ref: '../components/schemas/orders.yaml#/OrderTestList' - post: - tags: [Orders] + 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: @@ -123,51 +123,9 @@ '500': description: Server error - patch: - tags: [Orders] - summary: Update order - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - OrderID - properties: - OrderID: - type: string - 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/orders.yaml#/OrderTest' - delete: - tags: [Orders] + delete: + tags: [Order] summary: Delete order security: - bearerAuth: [] @@ -186,9 +144,9 @@ '200': description: Order deleted -/api/ordertest/status: - post: - tags: [Orders] +/api/ordertest/status: + post: + tags: [Order] summary: Update order status security: - bearerAuth: [] @@ -229,9 +187,9 @@ data: $ref: '../components/schemas/orders.yaml#/OrderTest' -/api/ordertest/{id}: - get: - tags: [Orders] +/api/ordertest/{id}: + get: + tags: [Order] summary: Get order by ID description: Returns order details with associated specimens and tests security: @@ -243,17 +201,63 @@ 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/orders.yaml#/OrderTest' + 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/orders.yaml#/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/orders.yaml#/OrderTest' diff --git a/public/paths/organization.yaml b/public/paths/organization.yaml index 1f165ec..fdddcfc 100644 --- a/public/paths/organization.yaml +++ b/public/paths/organization.yaml @@ -43,31 +43,6 @@ '201': description: Site created - patch: - tags: [Organization] - summary: Update site - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - SiteName: - type: string - SiteCode: - type: string - AccountID: - type: integer - responses: - '200': - description: Site updated delete: tags: [Organization] @@ -89,8 +64,8 @@ '200': description: Site deleted -/api/organization/site/{id}: - get: +/api/organization/site/{id}: + get: tags: [Organization] summary: Get site by ID security: @@ -101,9 +76,37 @@ required: true schema: type: integer - responses: - '200': - description: Site details + 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: @@ -130,35 +133,6 @@ '201': description: Discipline created - patch: - tags: [Organization] - summary: Update discipline - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - 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 - responses: - '200': - description: Discipline updated delete: tags: [Organization] @@ -180,8 +154,8 @@ '200': description: Discipline deleted -/api/organization/discipline/{id}: - get: +/api/organization/discipline/{id}: + get: tags: [Organization] summary: Get discipline by ID security: @@ -192,9 +166,41 @@ required: true schema: type: integer - responses: - '200': - description: Discipline details + 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: @@ -221,31 +227,6 @@ '201': description: Department created - patch: - tags: [Organization] - summary: Update department - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - DeptName: - type: string - DeptCode: - type: string - SiteID: - type: integer - responses: - '200': - description: Department updated delete: tags: [Organization] @@ -267,8 +248,8 @@ '200': description: Department deleted -/api/organization/department/{id}: - get: +/api/organization/department/{id}: + get: tags: [Organization] summary: Get department by ID security: @@ -279,9 +260,37 @@ required: true schema: type: integer - responses: - '200': - description: Department details + 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: @@ -308,33 +317,6 @@ '201': description: Workstation created - patch: - tags: [Organization] - summary: Update workstation - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - WorkstationName: - type: string - WorkstationCode: - type: string - SiteID: - type: integer - DepartmentID: - type: integer - responses: - '200': - description: Workstation updated delete: tags: [Organization] @@ -356,8 +338,8 @@ '200': description: Workstation deleted -/api/organization/workstation/{id}: - get: +/api/organization/workstation/{id}: + get: tags: [Organization] summary: Get workstation by ID security: @@ -368,9 +350,39 @@ required: true schema: type: integer - responses: - '200': - description: Workstation details + 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 # HostApp /api/organization/hostapp: @@ -420,29 +432,6 @@ '201': description: Host application created - patch: - tags: [Organization] - summary: Update host application - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - HostAppName: - type: string - SiteID: - type: integer - responses: - '200': - description: Host application updated delete: tags: [Organization] @@ -464,8 +453,8 @@ '200': description: Host application deleted -/api/organization/hostapp/{id}: - get: +/api/organization/hostapp/{id}: + get: tags: [Organization] summary: Get host application by ID security: @@ -476,13 +465,39 @@ required: true schema: type: string - responses: - '200': - description: Host application details - content: - application/json: - schema: - $ref: '../components/schemas/organization.yaml#/HostApp' + responses: + '200': + description: Host application details + content: + application/json: + schema: + $ref: '../components/schemas/organization.yaml#/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 # HostComPara /api/organization/hostcompara: @@ -532,31 +547,6 @@ '201': description: Host communication parameters created - patch: - tags: [Organization] - summary: Update host communication parameters - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - HostIP: - type: string - HostPort: - type: string - HostPwd: - type: string - responses: - '200': - description: Host communication parameters updated delete: tags: [Organization] @@ -578,8 +568,8 @@ '200': description: Host communication parameters deleted -/api/organization/hostcompara/{id}: - get: +/api/organization/hostcompara/{id}: + get: tags: [Organization] summary: Get host communication parameters by HostAppID security: @@ -590,13 +580,41 @@ required: true schema: type: string - responses: - '200': - description: Host communication parameters details - content: - application/json: - schema: - $ref: '../components/schemas/organization.yaml#/HostComPara' + responses: + '200': + description: Host communication parameters details + content: + application/json: + schema: + $ref: '../components/schemas/organization.yaml#/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 # CodingSys /api/organization/codingsys: @@ -646,31 +664,6 @@ '201': description: Coding system created - patch: - tags: [Organization] - summary: Update coding system - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - CodingSysID - properties: - CodingSysID: - type: integer - CodingSysAbb: - type: string - FullText: - type: string - Description: - type: string - responses: - '200': - description: Coding system updated delete: tags: [Organization] @@ -692,8 +685,8 @@ '200': description: Coding system deleted -/api/organization/codingsys/{id}: - get: +/api/organization/codingsys/{id}: + get: tags: [Organization] summary: Get coding system by ID security: @@ -704,10 +697,38 @@ required: true schema: type: integer - responses: - '200': - description: Coding system details - content: - application/json: - schema: - $ref: '../components/schemas/organization.yaml#/CodingSys' + responses: + '200': + description: Coding system details + content: + application/json: + schema: + $ref: '../components/schemas/organization.yaml#/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 diff --git a/public/paths/patient-visits.yaml b/public/paths/patient-visits.yaml index 3e699e7..96140c0 100644 --- a/public/paths/patient-visits.yaml +++ b/public/paths/patient-visits.yaml @@ -1,6 +1,6 @@ /api/patvisit: - get: - tags: [Patient Visits] + get: + tags: [Patient Visit] summary: List patient visits security: - bearerAuth: [] @@ -71,8 +71,8 @@ type: integer description: Number of records per page - post: - tags: [Patient Visits] + post: + tags: [Patient Visit] summary: Create patient visit description: | Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided. @@ -145,86 +145,9 @@ InternalPVID: type: integer - patch: - tags: [Patient Visits] - 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: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - InternalPVID - properties: - InternalPVID: - type: integer - description: Visit ID (required) - 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 - delete: - tags: [Patient Visits] + delete: + tags: [Patient Visit] summary: Delete patient visit security: - bearerAuth: [] @@ -232,9 +155,9 @@ '200': description: Visit deleted successfully -/api/patvisit/{id}: - get: - tags: [Patient Visits] +/api/patvisit/{id}: + get: + tags: [Patient Visit] summary: Get visit by ID security: - bearerAuth: [] @@ -245,24 +168,104 @@ 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/patient-visit.yaml#/PatientVisit' + responses: + '200': + description: Visit details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/patient-visit.yaml#/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 Visits] +/api/patvisit/patient/{patientId}: + get: + tags: [Patient Visit] summary: Get visits by patient ID security: - bearerAuth: [] @@ -288,9 +291,9 @@ items: $ref: '../components/schemas/patient-visit.yaml#/PatientVisit' -/api/patvisitadt: - post: - tags: [Patient Visits] +/api/patvisitadt: + post: + tags: [Patient Visit] summary: Create ADT record description: Create a new Admission/Discharge/Transfer record security: @@ -309,28 +312,10 @@ schema: $ref: '../components/schemas/common.yaml#/SuccessResponse' - patch: - tags: [Patient Visits] - summary: Update ADT record - description: Update an existing ADT record - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '../components/schemas/patient-visit.yaml#/PatVisitADT' - responses: - '200': - description: ADT record updated successfully - content: - application/json: - schema: - $ref: '../components/schemas/common.yaml#/SuccessResponse' + - delete: - tags: [Patient Visits] + delete: + tags: [Patient Visit] summary: Delete ADT visit (soft delete) security: - bearerAuth: [] @@ -350,9 +335,9 @@ '200': description: ADT visit deleted successfully -/api/patvisitadt/visit/{visitId}: - get: - tags: [Patient Visits] +/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: @@ -424,30 +409,9 @@ EndDate: type: string format: date-time - delete: - tags: [Patient Visits] - 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/{id}: - get: - tags: [Patient Visits] +/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: @@ -514,6 +478,33 @@ CreateDate: type: string format: date-time - EndDate: - 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/patient-visit.yaml#/PatVisitADT' + responses: + '200': + description: ADT record updated successfully + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/SuccessResponse' diff --git a/public/paths/patients.yaml b/public/paths/patients.yaml index 3288192..17d6f9e 100644 --- a/public/paths/patients.yaml +++ b/public/paths/patients.yaml @@ -1,6 +1,6 @@ /api/patient: - get: - tags: [Patients] + get: + tags: [Patient] summary: List patients security: - bearerAuth: [] @@ -44,8 +44,8 @@ schema: $ref: '../components/schemas/patient.yaml#/PatientListResponse' - post: - tags: [Patients] + post: + tags: [Patient] summary: Create new patient security: - bearerAuth: [] @@ -69,23 +69,9 @@ schema: $ref: '../components/schemas/common.yaml#/ErrorResponse' - patch: - tags: [Patients] - summary: Update patient - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '../components/schemas/patient.yaml#/Patient' - responses: - '200': - description: Patient updated successfully - delete: - tags: [Patients] + delete: + tags: [Patient] summary: Delete patient (soft delete) security: - bearerAuth: [] @@ -105,9 +91,9 @@ '200': description: Patient deleted successfully -/api/patient/check: - get: - tags: [Patients] +/api/patient/check: + get: + tags: [Patient] summary: Check if patient exists security: - bearerAuth: [] @@ -136,9 +122,9 @@ data: $ref: '../components/schemas/patient.yaml#/Patient' -/api/patient/{id}: - get: - tags: [Patients] +/api/patient/{id}: + get: + tags: [Patient] summary: Get patient by ID security: - bearerAuth: [] @@ -149,15 +135,37 @@ 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.yaml#/Patient' + responses: + '200': + description: Patient details + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + $ref: '../components/schemas/patient.yaml#/Patient' + + patch: + tags: [Patient] + summary: 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/patient.yaml#/Patient' + responses: + '200': + description: Patient updated successfully diff --git a/public/paths/reports.yaml b/public/paths/reports.yaml index 41d6d14..e18b961 100644 --- a/public/paths/reports.yaml +++ b/public/paths/reports.yaml @@ -1,6 +1,6 @@ -/api/reports/{orderID}: - get: - tags: [Reports] +/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: @@ -31,4 +31,4 @@ content: application/json: schema: - $ref: '../components/schemas/common.yaml#/ErrorResponse' \ No newline at end of file + $ref: '../components/schemas/common.yaml#/ErrorResponse' diff --git a/public/paths/results.yaml b/public/paths/results.yaml index 7e3a775..71d7569 100644 --- a/public/paths/results.yaml +++ b/public/paths/results.yaml @@ -1,6 +1,6 @@ -/api/results: - get: - tags: [Results] +/api/result: + get: + tags: [Result] summary: List results description: Retrieve patient test results with optional filters by order or patient security: @@ -94,9 +94,9 @@ type: string nullable: true -/api/results/{id}: - get: - tags: [Results] +/api/result/{id}: + get: + tags: [Result] summary: Get result by ID description: Retrieve a specific result entry with all related data security: @@ -202,8 +202,8 @@ schema: $ref: '../components/schemas/common.yaml#/ErrorResponse' - patch: - tags: [Results] + 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: @@ -273,8 +273,8 @@ schema: $ref: '../components/schemas/common.yaml#/ErrorResponse' - delete: - tags: [Results] + delete: + tags: [Result] summary: Delete result description: Soft delete a result entry by setting DelDate security: @@ -298,4 +298,4 @@ content: application/json: schema: - $ref: '../components/schemas/common.yaml#/ErrorResponse' \ No newline at end of file + $ref: '../components/schemas/common.yaml#/ErrorResponse' diff --git a/public/paths/rules.yaml b/public/paths/rules.yaml index a8aeb0c..43bbe3b 100644 --- a/public/paths/rules.yaml +++ b/public/paths/rules.yaml @@ -1,6 +1,6 @@ -/api/rules: - get: - tags: [Rules] +/api/rule: + get: + tags: [Rule] summary: List rules security: - bearerAuth: [] @@ -38,7 +38,7 @@ $ref: '../components/schemas/rules.yaml#/RuleDef' post: - tags: [Rules] + tags: [Rule] summary: Create rule description: | Create a new rule. Rules must be linked to at least one test via TestSiteIDs. @@ -77,15 +77,15 @@ ConditionExprCompiled: type: string nullable: true - description: Compiled JSON payload from POST /api/rules/compile + description: Compiled JSON payload from POST /api/rule/compile required: [RuleCode, RuleName, EventCode, TestSiteIDs] responses: '201': description: Rule created -/api/rules/{id}: +/api/rule/{id}: get: - tags: [Rules] + tags: [Rule] summary: Get rule with linked tests security: - bearerAuth: [] @@ -113,8 +113,8 @@ '404': description: Rule not found - patch: - tags: [Rules] + patch: + tags: [Rule] summary: Update rule description: | Update a rule. TestSiteIDs can be provided to update which tests the rule is linked to. @@ -152,8 +152,8 @@ '404': description: Rule not found - delete: - tags: [Rules] + delete: + tags: [Rule] summary: Soft delete rule security: - bearerAuth: [] @@ -170,9 +170,9 @@ '404': description: Rule not found -/api/rules/validate: - post: - tags: [Rules] +/api/rule/validate: + post: + tags: [Rule] summary: Validate/evaluate an expression security: - bearerAuth: [] @@ -193,14 +193,14 @@ '200': description: Validation result -/api/rules/compile: - post: - tags: [Rules] +/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. + Returns compiled structure that can be saved to ConditionExprCompiled field. security: - bearerAuth: [] requestBody: diff --git a/public/paths/specimen.yaml b/public/paths/specimen.yaml index 4b9aa88..e13ac0f 100644 --- a/public/paths/specimen.yaml +++ b/public/paths/specimen.yaml @@ -23,23 +23,9 @@ '201': description: Specimen created - patch: - tags: [Specimen] - summary: Update specimen - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '../components/schemas/specimen.yaml#/Specimen' - responses: - '200': - description: Specimen updated -/api/specimen/{id}: - get: +/api/specimen/{id}: + get: tags: [Specimen] summary: Get specimen by ID security: @@ -50,9 +36,31 @@ required: true schema: type: integer - responses: - '200': - description: Specimen details + 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.yaml#/Specimen' + responses: + '200': + description: Specimen updated delete: tags: [Specimen] @@ -141,17 +149,9 @@ '201': description: Container definition created - patch: - tags: [Specimen] - summary: Update container definition - security: - - bearerAuth: [] - responses: - '200': - description: Container definition updated -/api/specimen/container/{id}: - get: +/api/specimen/container/{id}: + get: tags: [Specimen] summary: Get container definition by ID security: @@ -162,9 +162,31 @@ required: true schema: type: integer - responses: - '200': - description: Container definition details + 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/specimen.yaml#/ContainerDef' + responses: + '200': + description: Container definition updated /api/specimen/containerdef: get: @@ -191,14 +213,29 @@ '201': description: Container definition created - patch: - tags: [Specimen] - summary: Update container definition (alias) - security: - - bearerAuth: [] - responses: - '200': - description: Container definition updated + +/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/specimen.yaml#/ContainerDef' + responses: + '200': + description: Container definition updated /api/specimen/prep: get: @@ -225,17 +262,9 @@ '201': description: Specimen preparation created - patch: - tags: [Specimen] - summary: Update specimen preparation - security: - - bearerAuth: [] - responses: - '200': - description: Specimen preparation updated -/api/specimen/prep/{id}: - get: +/api/specimen/prep/{id}: + get: tags: [Specimen] summary: Get specimen preparation by ID security: @@ -246,9 +275,31 @@ required: true schema: type: integer - responses: - '200': - description: Specimen preparation details + 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/specimen.yaml#/SpecimenPrep' + responses: + '200': + description: Specimen preparation updated /api/specimen/status: get: @@ -275,17 +326,9 @@ '201': description: Specimen status created - patch: - tags: [Specimen] - summary: Update specimen status - security: - - bearerAuth: [] - responses: - '200': - description: Specimen status updated -/api/specimen/status/{id}: - get: +/api/specimen/status/{id}: + get: tags: [Specimen] summary: Get specimen status by ID security: @@ -296,9 +339,31 @@ required: true schema: type: integer - responses: - '200': - description: Specimen status details + 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/specimen.yaml#/SpecimenStatus' + responses: + '200': + description: Specimen status updated /api/specimen/collection: get: @@ -325,17 +390,9 @@ '201': description: Collection method created - patch: - tags: [Specimen] - summary: Update specimen collection method - security: - - bearerAuth: [] - responses: - '200': - description: Collection method updated -/api/specimen/collection/{id}: - get: +/api/specimen/collection/{id}: + get: tags: [Specimen] summary: Get specimen collection method by ID security: @@ -346,6 +403,28 @@ required: true schema: type: integer - responses: - '200': - description: Collection method details + 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/specimen.yaml#/SpecimenCollection' + responses: + '200': + description: Collection method updated diff --git a/public/paths/testmap.yaml b/public/paths/testmap.yaml index 71392bb..a90dd2c 100644 --- a/public/paths/testmap.yaml +++ b/public/paths/testmap.yaml @@ -1,6 +1,6 @@ /api/test/testmap: get: - tags: [Tests] + tags: [Test] summary: List all test mappings security: - bearerAuth: [] @@ -37,8 +37,8 @@ ClientName: type: string - post: - tags: [Tests] + post: + tags: [Test] summary: Create test mapping (header only) security: - bearerAuth: [] @@ -99,53 +99,9 @@ type: integer description: Created TestMapID - patch: - tags: [Tests] - summary: Update test mapping - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapID: - type: integer - description: Test Map ID (required) - TestCode: - type: string - description: Test Code - maps to HostTestCode or ClientTestCode - HostType: - type: string - HostID: - type: string - ClientType: - type: string - ClientID: - type: string - required: - - TestMapID - 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 - delete: - tags: [Tests] + delete: + tags: [Test] summary: Soft delete test mapping (cascades to details) security: - bearerAuth: [] @@ -180,9 +136,9 @@ '404': description: Test mapping not found or already deleted -/api/test/testmap/{id}: - get: - tags: [Tests] +/api/test/testmap/{id}: + get: + tags: [Test] summary: Get test mapping by ID with details security: - bearerAuth: [] @@ -193,26 +149,73 @@ 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/tests.yaml#/TestMap' - '404': - description: Test mapping not found + 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/tests.yaml#/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: + TestCode: + type: string + description: Test Code - maps to HostTestCode or ClientTestCode + HostType: + type: string + HostID: + type: string + ClientType: + type: string + ClientID: + 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: [Tests] + tags: [Test] summary: Get test mappings by test code with details security: - bearerAuth: [] @@ -242,7 +245,7 @@ /api/test/testmap/detail: get: - tags: [Tests] + tags: [Test] summary: List test mapping details security: - bearerAuth: [] @@ -270,7 +273,7 @@ $ref: '../components/schemas/tests.yaml#/TestMapDetail' post: - tags: [Tests] + tags: [Test] summary: Create test mapping detail security: - bearerAuth: [] @@ -312,41 +315,9 @@ type: integer description: Created TestMapDetailID - patch: - tags: [Tests] - summary: Update test mapping detail - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapDetailID: - type: integer - description: Test Map Detail ID (required) - TestMapID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - required: - - TestMapDetailID - responses: - '200': - description: Test mapping detail updated delete: - tags: [Tests] + tags: [Test] summary: Soft delete test mapping detail security: - bearerAuth: [] @@ -366,9 +337,9 @@ '200': description: Test mapping detail deleted -/api/test/testmap/detail/{id}: - get: - tags: [Tests] +/api/test/testmap/detail/{id}: + get: + tags: [Test] summary: Get test mapping detail by ID security: - bearerAuth: [] @@ -379,24 +350,59 @@ 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/tests.yaml#/TestMapDetail' + responses: + '200': + description: Test mapping detail + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/tests.yaml#/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: [Tests] + tags: [Test] summary: Get test mapping details by test map ID security: - bearerAuth: [] @@ -426,7 +432,7 @@ /api/test/testmap/detail/batch: post: - tags: [Tests] + tags: [Test] summary: Batch create test mapping details security: - bearerAuth: [] @@ -456,7 +462,7 @@ description: Batch create results patch: - tags: [Tests] + tags: [Test] summary: Batch update test mapping details security: - bearerAuth: [] @@ -488,7 +494,7 @@ description: Batch update results delete: - tags: [Tests] + tags: [Test] summary: Batch delete test mapping details security: - bearerAuth: [] diff --git a/public/paths/tests.yaml b/public/paths/tests.yaml index 62ee3ec..2e0eeea 100644 --- a/public/paths/tests.yaml +++ b/public/paths/tests.yaml @@ -1,6 +1,6 @@ /api/test: get: - tags: [Tests] + tags: [Test] summary: List test definitions security: - bearerAuth: [] @@ -67,7 +67,7 @@ description: Total number of records matching the query post: - tags: [Tests] + tags: [Test] summary: Create test definition security: - bearerAuth: [] @@ -205,29 +205,462 @@ - TestSiteName - TestType examples: - CALC_test: - summary: Create calculated test with members + TEST_no_ref: + summary: Technical test without reference or map value: SiteID: 1 - TestSiteCode: IBIL - TestSiteName: Indirect Bilirubin + TestSiteCode: TEST_NREF + TestSiteName: Numeric Test + TestType: TEST + SeqScr: 500 + SeqRpt: 500 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + 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 + VisibleScr: 1 + VisibleRpt: 0 + CountStat: 0 + details: + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 18 + AgeEnd: 99 + Flag: N + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 18 + AgeEnd: 99 + Flag: N + - NumRefType: NMRC + RangeType: REF + Sex: '1' + LowSign: '>' + Low: 75 + HighSign: '<' + High: 105 + AgeStart: 18 + AgeEnd: 99 + Flag: N + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '1' + details: + - HostTestCode: GLU + HostTestName: Glucose + ConDefID: 1 + ClientTestCode: GLU_C + ClientTestName: Glucose Client + - HostTestCode: CREA + HostTestName: Creatinine + ConDefID: 2 + ClientTestCode: CREA_C + ClientTestName: Creatinine Client + - HostType: WST + HostID: '3' + ClientType: INST + ClientID: '2' + details: + - HostTestCode: HB + HostTestName: Hemoglobin + ConDefID: 3 + ClientTestCode: HB_C + ClientTestName: Hemoglobin Client + TEST_threshold: + summary: Technical test with threshold reference + value: + SiteID: 1 + TestSiteCode: TEST_THLD + TestSiteName: Sodium Threshold + TestType: TEST + SeqScr: 115 + SeqRpt: 115 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 125 + Flag: H + TEST_threshold_map: + summary: Threshold reference plus test map + value: + SiteID: 1 + TestSiteCode: TEST_TMAP + TestSiteName: Potassium Panic + TestType: TEST + SeqScr: 120 + SeqRpt: 120 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 125 + Flag: H + - NumRefType: THOLD + RangeType: PANIC + Sex: '1' + LowSign: '<' + Low: 121 + AgeStart: 0 + AgeEnd: 125 + 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 + TEST_text: + summary: Technical test with text reference + value: + SiteID: 1 + TestSiteCode: TEST_TEXT + TestSiteName: Disease Stage + TestType: TEST + SeqScr: 130 + SeqRpt: 130 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + Method: Morphology + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 18 + AgeEnd: 99 + RefTxt: 'NORM=Normal;HIGH=High' + Flag: N + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 18 + AgeEnd: 99 + RefTxt: 'NORM=Normal' + Flag: N + - SpcType: GEN + TxtRefType: TEXT + Sex: '1' + AgeStart: 18 + AgeEnd: 99 + 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 + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + Method: Visual + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 120 + RefTxt: 'NORM=Normal;MACRO=Macro' + Flag: N + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 120 + 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 + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '8' + details: + - HostTestCode: UGLUC + HostTestName: Urine Glucose + ConDefID: 12 + ClientTestCode: UGLUC_C + ClientTestName: Urine Glucose Client + CALC_basic: + summary: Calculated test with members (no references) + value: + SiteID: 1 + TestSiteCode: CALC_BASE + TestSiteName: Estimated GFR TestType: CALC - Description: Bilirubin Indirek - SeqScr: 210 - SeqRpt: 210 + SeqScr: 190 + SeqRpt: 190 VisibleScr: 1 VisibleRpt: 1 CountStat: 0 details: DisciplineID: 2 DepartmentID: 2 - FormulaCode: "{TBIL} - {DBIL}" - RefType: RANGE - Unit1: mg/dL - Decimal: 2 + FormulaCode: CKD_EPI(CREA,AGE,GENDER) members: + - TestSiteID: 21 - TestSiteID: 22 - - TestSiteID: 23 + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 + details: + DisciplineID: 2 + DepartmentID: 2 + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + members: + - TestSiteID: 21 + - TestSiteID: 22 + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 10 + HighSign: LE + High: 20 + AgeStart: 18 + AgeEnd: 120 + Flag: N + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: EGFR + HostTestName: eGFR + ConDefID: 1 + ClientTestCode: EGFR_C + ClientTestName: eGFR Client + 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 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + members: + - TestSiteID: 169 + - TestSiteID: 170 + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: LIPID + HostTestName: Lipid Profile + ConDefID: 1 + ClientTestCode: LIPID_C + ClientTestName: Lipid Client responses: '201': description: Test definition created @@ -261,7 +694,7 @@ example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' patch: - tags: [Tests] + tags: [Test] summary: Update test definition security: - bearerAuth: [] @@ -426,7 +859,7 @@ /api/test/{id}: get: - tags: [Tests] + tags: [Test] summary: Get test definition by ID security: - bearerAuth: [] @@ -455,7 +888,7 @@ description: Test not found delete: - tags: [Tests] + tags: [Test] summary: Soft delete test definition security: - bearerAuth: [] diff --git a/public/paths/users.yaml b/public/paths/users.yaml index e3651bd..8910a79 100644 --- a/public/paths/users.yaml +++ b/public/paths/users.yaml @@ -1,6 +1,6 @@ -/api/users: +/api/user: get: - tags: [Users] + tags: [User] summary: List users with pagination and search security: - bearerAuth: [] @@ -58,7 +58,7 @@ description: Server error post: - tags: [Users] + tags: [User] summary: Create new user security: - bearerAuth: [] @@ -109,11 +109,44 @@ '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: [Users] + 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: @@ -150,33 +183,8 @@ '500': description: Server error -/api/users/{id}: - get: - tags: [Users] - 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 - delete: - tags: [Users] + tags: [User] summary: Delete user (soft delete) security: - bearerAuth: [] @@ -209,4 +217,4 @@ '404': description: User not found '500': - description: Server error \ No newline at end of file + description: Server error diff --git a/public/paths/valuesets.yaml b/public/paths/valuesets.yaml index 7127937..1d14daa 100644 --- a/public/paths/valuesets.yaml +++ b/public/paths/valuesets.yaml @@ -1,6 +1,6 @@ /api/valueset: - get: - tags: [ValueSets] + 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: @@ -37,9 +37,9 @@ label: Order Status count: 6 -/api/valueset/{key}: - get: - tags: [ValueSets] +/api/valueset/{key}: + get: + tags: [ValueSet] summary: Get lib value set by key description: | Get a specific library/system value set from JSON files. @@ -117,9 +117,9 @@ items: $ref: '../components/schemas/valuesets.yaml#/ValueSetLibItem' -/api/valueset/refresh: - post: - tags: [ValueSets] +/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: @@ -139,9 +139,9 @@ type: string example: Cache cleared -/api/valueset/user/items: - get: - tags: [ValueSets] +/api/valueset/user/items: + get: + tags: [ValueSet] summary: List user value set items description: List value set items from database (user-defined) security: @@ -177,8 +177,8 @@ items: $ref: '../components/schemas/valuesets.yaml#/ValueSetItem' - post: - tags: [ValueSets] + post: + tags: [ValueSet] summary: Create user value set item description: Create value set item in database (user-defined) security: @@ -222,9 +222,9 @@ data: $ref: '../components/schemas/valuesets.yaml#/ValueSetItem' -/api/valueset/user/items/{id}: - get: - tags: [ValueSets] +/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: @@ -248,8 +248,8 @@ data: $ref: '../components/schemas/valuesets.yaml#/ValueSetItem' - put: - tags: [ValueSets] + put: + tags: [ValueSet] summary: Update user value set item description: Update value set item in database (user-defined) security: @@ -297,8 +297,8 @@ data: $ref: '../components/schemas/valuesets.yaml#/ValueSetItem' - delete: - tags: [ValueSets] + delete: + tags: [ValueSet] summary: Delete user value set item description: Delete value set item from database (user-defined) security: @@ -322,9 +322,9 @@ message: type: string -/api/valueset/user/def: - get: - tags: [ValueSets] +/api/valueset/user/def: + get: + tags: [ValueSet] summary: List user value set definitions description: List value set definitions from database (user-defined) security: @@ -371,8 +371,8 @@ limit: type: integer - post: - tags: [ValueSets] + post: + tags: [ValueSet] summary: Create user value set definition description: Create value set definition in database (user-defined) security: @@ -408,9 +408,9 @@ data: $ref: '../components/schemas/valuesets.yaml#/ValueSetDef' -/api/valueset/user/def/{id}: - get: - tags: [ValueSets] +/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: @@ -434,8 +434,8 @@ data: $ref: '../components/schemas/valuesets.yaml#/ValueSetDef' - put: - tags: [ValueSets] + put: + tags: [ValueSet] summary: Update user value set definition description: Update value set definition in database (user-defined) security: @@ -477,8 +477,8 @@ data: $ref: '../components/schemas/valuesets.yaml#/ValueSetDef' - delete: - tags: [ValueSets] + delete: + tags: [ValueSet] summary: Delete user value set definition description: Delete value set definition from database (user-defined) security: diff --git a/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php b/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php deleted file mode 100644 index bef8be0..0000000 --- a/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php +++ /dev/null @@ -1,37 +0,0 @@ -forge->addField('id'); - $this->forge->addField([ - 'name' => ['type' => 'varchar', 'constraint' => 31], - 'uid' => ['type' => 'varchar', 'constraint' => 31], - 'class' => ['type' => 'varchar', 'constraint' => 63], - 'icon' => ['type' => 'varchar', 'constraint' => 31], - 'summary' => ['type' => 'varchar', 'constraint' => 255], - 'created_at' => ['type' => 'datetime', 'null' => true], - 'updated_at' => ['type' => 'datetime', 'null' => true], - 'deleted_at' => ['type' => 'datetime', 'null' => true], - ]); - - $this->forge->addKey('name'); - $this->forge->addKey('uid'); - $this->forge->addKey(['deleted_at', 'id']); - $this->forge->addKey('created_at'); - - $this->forge->createTable('factories'); - } - - public function down(): void - { - $this->forge->dropTable('factories'); - } -} diff --git a/tests/_support/Traits/CreatesPatients.php b/tests/_support/Traits/CreatesPatients.php new file mode 100644 index 0000000..7b1be30 --- /dev/null +++ b/tests/_support/Traits/CreatesPatients.php @@ -0,0 +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', + 'DeathIndicator' => (string) $faker->numberBetween(16, 17), + 'PatIdt' => [ + 'IdentifierType' => 'ID', + 'Identifier' => $faker->numerify('################') + ], + 'PatAtt' => [ + [ 'Address' => '/api/upload/' . $faker->uuid . '.jpg' ] + ], + 'PatCom' => $faker->sentence, + ], $overrides); + + if ($patientPayload['DeathIndicator'] === '16') { + $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/Calculator/CalculatorEndpointTest.php b/tests/feature/Calculator/CalculatorEndpointTest.php deleted file mode 100644 index 9b81331..0000000 --- a/tests/feature/Calculator/CalculatorEndpointTest.php +++ /dev/null @@ -1,148 +0,0 @@ -siteModel = new TestDefSiteModel(); - $this->calcModel = new TestDefCalModel(); - $this->calcName = 'API Calc ' . uniqid(); - $this->calcCode = $this->generateUniqueCalcCode(); - - $siteId = $this->siteModel->insert([ - 'SiteID' => 1, - 'TestSiteCode' => $this->calcCode, - 'TestSiteName' => $this->calcName, - 'TestType' => 'CALC', - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE', - 'Unit1' => 'mg/dL', - 'VisibleScr' => 1, - 'VisibleRpt' => 1, - 'CountStat' => 0, - 'CreateDate' => date('Y-m-d H:i:s'), - 'StartDate' => date('Y-m-d H:i:s'), - ]); - - $this->assertNotFalse($siteId, 'Failed to insert testdefsite'); - $this->siteId = $siteId; - - $this->calcId = $this->calcModel->insert([ - 'TestSiteID' => $siteId, - 'DisciplineID' => 1, - 'DepartmentID' => 1, - 'FormulaCode' => 'TBIL - DBIL', - 'RefType' => 'RANGE', - 'Unit1' => 'mg/dL', - 'Factor' => 1, - 'Decimal' => 2, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - $this->assertNotFalse($this->calcId, 'Failed to insert testdefcal'); - - $this->assertNotNull($this->calcModel->findActiveByCodeOrName($this->calcCode)); - - } - - protected function tearDown(): void - { - if ($this->calcId) { - $this->calcModel->delete($this->calcId); - } - - if ($this->siteId) { - $this->siteModel->delete($this->siteId); - } - - parent::tearDown(); - } - - public function testCalculateByCodeReturnsValue() - { - $response = $this->postCalc($this->calcCode, ['TBIL' => 5, 'DBIL' => 3]); - $response->assertStatus(200); - - $data = $this->decodeResponse($response); - - $this->assertArrayHasKey($this->calcCode, $data); - $this->assertEquals(2.0, $data[$this->calcCode]); - } - - public function testCalculateByNameReturnsValue() - { - $response = $this->postCalc($this->calcName, ['TBIL' => 4, 'DBIL' => 1]); - $response->assertStatus(200); - - $data = $this->decodeResponse($response); - - $this->assertArrayHasKey($this->calcCode, $data); - $this->assertEquals(3.0, $data[$this->calcCode]); - } - - public function testIncompletePayloadReturnsEmptyObject() - { - $response = $this->postCalc($this->calcCode, ['TBIL' => 5]); - $response->assertStatus(200); - - $data = $this->decodeResponse($response); - $this->assertSame([], $data); - } - - public function testUnknownCalculatorReturnsEmptyObject() - { - $response = $this->postCalc('UNKNOWN_CALC', ['TBIL' => 3, 'DBIL' => 1]); - $response->assertStatus(200); - - $data = $this->decodeResponse($response); - $this->assertSame([], $data); - } - - private function postCalc(string $identifier, array $payload) - { - return $this->withHeaders(['Content-Type' => 'application/json']) - ->withBody(json_encode($payload)) - ->call('post', 'api/calc/' . rawurlencode($identifier)); - } - - private function decodeResponse($response): array - { - $json = $response->getJSON(); - if (empty($json)) { - return []; - } - - return json_decode($json, true) ?: []; - } - - private function generateUniqueCalcCode(): string - { - $tries = 0; - do { - $code = 'TC' . strtoupper(bin2hex(random_bytes(2))); - $exists = $this->siteModel->where('TestSiteCode', $code) - ->where('EndDate IS NULL') - ->first(); - } while ($exists && ++$tries < 20); - - return $code; - } -} diff --git a/tests/feature/Calculator/CalculatorTest.php b/tests/feature/Calculator/CalculatorTest.php deleted file mode 100644 index 7f15347..0000000 --- a/tests/feature/Calculator/CalculatorTest.php +++ /dev/null @@ -1,250 +0,0 @@ -calculator = new CalculatorService(); - } - - /** - * Test basic arithmetic operations - */ - public function testBasicArithmetic() { - // Addition and multiplication precedence - $result = $this->calculator->calculate('1+2*3'); - $this->assertEquals(7.0, $result); - - // Parentheses - $result = $this->calculator->calculate('(1+2)*3'); - $this->assertEquals(9.0, $result); - - // Division - $result = $this->calculator->calculate('10/2'); - $this->assertEquals(5.0, $result); - - // Power - $result = $this->calculator->calculate('2^3'); - $this->assertEquals(8.0, $result); - } - - /** - * Test formula with simple variables - */ - public function testFormulaWithVariables() { - $formula = '{result} * {factor}'; - $variables = ['result' => 50, 'factor' => 2]; - - $result = $this->calculator->calculate($formula, $variables); - $this->assertEquals(100.0, $result); - } - - /** - * Test formula with gender variable (numeric values) - */ - public function testFormulaWithGenderNumeric() { - // Gender: 0=Unknown, 1=Female, 2=Male - $formula = '50 + {gender} * 10'; - - // Male (2) - $result = $this->calculator->calculate($formula, ['gender' => 2]); - $this->assertEquals(70.0, $result); - - // Female (1) - $result = $this->calculator->calculate($formula, ['gender' => 1]); - $this->assertEquals(60.0, $result); - - // Unknown (0) - $result = $this->calculator->calculate($formula, ['gender' => 0]); - $this->assertEquals(50.0, $result); - } - - /** - * Test formula with gender variable (string values) - */ - public function testFormulaWithGenderString() { - $formula = '50 + {gender} * 10'; - - // String values - $result = $this->calculator->calculate($formula, ['gender' => 'male']); - $this->assertEquals(70.0, $result); - - $result = $this->calculator->calculate($formula, ['gender' => 'female']); - $this->assertEquals(60.0, $result); - - $result = $this->calculator->calculate($formula, ['gender' => 'unknown']); - $this->assertEquals(50.0, $result); - } - - /** - * Test mathematical functions - */ - public function testMathFunctions() { - // Square root - $result = $this->calculator->calculate('sqrt(16)'); - $this->assertEquals(4.0, $result); - - // Sine - $result = $this->calculator->calculate('sin(pi/2)'); - $this->assertEqualsWithDelta(1.0, $result, 0.0001); - - // Cosine - $result = $this->calculator->calculate('cos(0)'); - $this->assertEquals(1.0, $result); - - // Logarithm - $result = $this->calculator->calculate('log(100)'); - $this->assertEqualsWithDelta(4.60517, $result, 0.0001); - - // Natural log (ln) - $result = $this->calculator->calculate('ln(2.71828)'); - $this->assertEqualsWithDelta(1.0, $result, 0.0001); - - // Exponential - $result = $this->calculator->calculate('exp(1)'); - $this->assertEqualsWithDelta(2.71828, $result, 0.0001); - } - - /** - * Test formula validation - */ - public function testFormulaValidation() { - // Valid formula - $validation = $this->calculator->validate('{result} * 2 + 5'); - $this->assertTrue($validation['valid']); - $this->assertNull($validation['error']); - - // Invalid formula - $validation = $this->calculator->validate('{result} * * 2'); - $this->assertFalse($validation['valid']); - $this->assertNotNull($validation['error']); - } - - /** - * Test variable extraction - */ - public function testExtractVariables() { - $formula = '{result} * {factor} + {gender} - {age}'; - $variables = $this->calculator->extractVariables($formula); - - $this->assertEquals(['result', 'factor', 'gender', 'age'], $variables); - } - - /** - * Test missing variable error - */ - public function testMissingVariableError() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Missing variable value for: missing_var"); - - $this->calculator->calculate('{result} + {missing_var}', ['result' => 10]); - } - - /** - * Test invalid formula syntax error - */ - public function testInvalidFormulaError() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Invalid formula"); - - $this->calculator->calculate('1 + * 2'); - } - - /** - * Test complex formula with multiple variables - */ - public function testComplexFormula() { - // Complex formula: (result * factor / 100) + (gender * 5) - (age * 0.1) - $formula = '({result} * {factor} / 100) + ({gender} * 5) - ({age} * 0.1)'; - $variables = [ - 'result' => 200, - 'factor' => 10, - 'gender' => 2, // Male - 'age' => 30 - ]; - - // Expected: (200 * 10 / 100) + (2 * 5) - (30 * 0.1) = 20 + 10 - 3 = 27 - $result = $this->calculator->calculate($formula, $variables); - $this->assertEquals(27.0, $result); - } - - /** - * Test calculation from TestDefCal definition - */ - public function testCalculateFromDefinition() { - $calcDef = [ - 'FormulaCode' => '{result} * {factor} + 10', - 'Factor' => 2, - ]; - - $testValues = [ - 'result' => 50, - ]; - - // Expected: 50 * 2 + 10 = 110 - $result = $this->calculator->calculateFromDefinition($calcDef, $testValues); - $this->assertEquals(110.0, $result); - } - - /** - * Test calculation with all optional variables - */ - public function testCalculateWithAllVariables() { - $calcDef = [ - 'FormulaCode' => '{result} + {factor} + {gender} + {age} + {ref_low} + {ref_high}', - 'Factor' => 5, - ]; - - $testValues = [ - 'result' => 10, - 'gender' => 1, - 'age' => 25, - 'ref_low' => 5, - 'ref_high' => 15, - ]; - - // Expected: 10 + 5 + 1 + 25 + 5 + 15 = 61 - $result = $this->calculator->calculateFromDefinition($calcDef, $testValues); - $this->assertEquals(61.0, $result); - } - - /** - * Test empty formula error - */ - public function testEmptyFormulaError() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("No formula defined"); - - $calcDef = [ - 'FormulaCode' => '', - 'Factor' => 1, - ]; - - $this->calculator->calculateFromDefinition($calcDef, ['result' => 10]); - } - - /** - * Test implicit multiplication - */ - public function testImplicitMultiplication() { - // math-parser supports implicit multiplication (2x means 2*x) - $result = $this->calculator->calculate('2*3'); - $this->assertEquals(6.0, $result); - } - - /** - * Test decimal calculations - */ - public function testDecimalCalculations() { - $formula = '{result} / 3'; - $result = $this->calculator->calculate($formula, ['result' => 10]); - $this->assertEqualsWithDelta(3.33333, $result, 0.0001); - } -} diff --git a/tests/feature/ContactControllerTest.php b/tests/feature/ContactControllerTest.php index 5ff5c78..3fdd6b7 100644 --- a/tests/feature/ContactControllerTest.php +++ b/tests/feature/ContactControllerTest.php @@ -15,8 +15,8 @@ class ContactControllerTest extends CIUnitTestCase protected function setUp(): void { parent::setUp(); - - // Generate JWT Token + + // Generate JWT Token $key = getenv('JWT_SECRET') ?: 'my-secret-key'; $payload = [ 'iss' => 'localhost', @@ -27,8 +27,8 @@ class ContactControllerTest extends CIUnitTestCase 'uid' => 1, 'email' => 'admin@admin.com' ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } + $this->token = JWT::encode($payload, $key, 'HS256'); + } protected function callProtected($method, $path, $params = []) { diff --git a/tests/feature/Orders/OrderCreateTest.php b/tests/feature/Orders/OrderCreateTest.php deleted file mode 100644 index 33e9d10..0000000 --- a/tests/feature/Orders/OrderCreateTest.php +++ /dev/null @@ -1,285 +0,0 @@ -createOrderTestPatient(); - - // Get available tests from testdefsite - $testsResult = $this->call('get', 'api/test'); - $testsBody = json_decode($testsResult->getBody(), true); - $availableTests = $testsBody['data'] ?? []; - - // Skip if no tests available - if (empty($availableTests)) { - $this->markTestSkipped('No tests available in testdefsite table'); - } - - $testSiteID = $availableTests[0]['TestSiteID']; - - // Create order with tests - $payload = [ - 'InternalPID' => $internalPID, - 'Priority' => 'R', - 'Tests' => [ - ['TestSiteID' => $testSiteID] - ] - ]; - - $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - - // Assertions - $result->assertStatus(201); - - $body = json_decode($result->getBody(), true); - - $this->assertEquals('success', $body['status']); - $this->assertArrayHasKey('data', $body); - $this->assertArrayHasKey('OrderID', $body['data']); - $this->assertArrayHasKey('Specimens', $body['data']); - $this->assertArrayHasKey('Tests', $body['data']); - $this->assertIsArray($body['data']['Specimens']); - $this->assertIsArray($body['data']['Tests']); - $this->assertNotEmpty($body['data']['Tests'], 'Tests array should not be empty'); - - return $body['data']['OrderID']; - } - - public function testCreateOrderValidationFailsWithoutInternalPID() - { - $payload = [ - 'Tests' => [ - ['TestSiteID' => 1] - ] - ]; - - $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - - $result->assertStatus(400); - - $body = json_decode(strip_tags($result->getBody()), true); - $this->assertIsArray($body); - $messages = $body['messages'] ?? $body['errors'] ?? []; - $this->assertIsArray($messages); - $this->assertArrayHasKey('InternalPID', $messages); - } - - public function testCreateOrderFailsWithInvalidPatient() - { - $payload = [ - 'InternalPID' => 999999, - 'Tests' => [ - ['TestSiteID' => 1] - ] - ]; - - $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - - $result->assertStatus(400); - - $body = json_decode(strip_tags($result->getBody()), true); - $this->assertIsArray($body); - $messages = $body['messages'] ?? $body['errors'] ?? []; - $this->assertIsArray($messages); - $this->assertArrayHasKey('InternalPID', $messages); - } - - public function testCreateOrderWithMultipleTests() - { - $internalPID = $this->createOrderTestPatient(); - - // Get available tests - $testsResult = $this->call('get', 'api/test'); - $testsBody = json_decode($testsResult->getBody(), true); - $availableTests = $testsBody['data'] ?? []; - - if (count($availableTests) < 2) { - $this->markTestSkipped('Need at least 2 tests for this test'); - } - - $testSiteID1 = $availableTests[0]['TestSiteID']; - $testSiteID2 = $availableTests[1]['TestSiteID']; - - // Create order with multiple tests - $payload = [ - 'InternalPID' => $internalPID, - 'Priority' => 'S', - 'Comment' => 'Urgent order for multiple tests', - 'Tests' => [ - ['TestSiteID' => $testSiteID1], - ['TestSiteID' => $testSiteID2] - ] - ]; - - $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - - $result->assertStatus(201); - - $body = json_decode($result->getBody(), true); - - $this->assertEquals('success', $body['status']); - $this->assertGreaterThanOrEqual(1, count($body['data']['Specimens']), 'Should have at least one specimen'); - $this->assertGreaterThanOrEqual(2, count($body['data']['Tests']), 'Should have at least two tests'); - } - - public function testOrderShowIncludesDisciplineAndSequenceOrdering() - { - $internalPID = $this->createOrderTestPatient(); - $testSiteIDs = $this->collectTestSiteIDs(2); - - $orderID = $this->createOrderWithTests($internalPID, $testSiteIDs); - - $response = $this->call('get', $this->endpoint . '/' . $orderID); - $response->assertStatus(200); - - $body = json_decode($response->getBody(), true); - $this->assertEquals('success', $body['status']); - - $tests = $body['data']['Tests'] ?? []; - $this->assertNotEmpty($tests, 'Tests payload should not be empty'); - - $lastKey = null; - foreach ($tests as $test) { - $this->assertArrayHasKey('Discipline', $test); - $this->assertArrayHasKey('TestType', $test); - $this->assertNotEmpty($test['TestType'], 'Each test should report a test type'); - $this->assertArrayHasKey('SeqScr', $test); - $this->assertArrayHasKey('SeqRpt', $test); - - $discipline = $test['Discipline']; - $this->assertArrayHasKey('DisciplineID', $discipline); - $this->assertArrayHasKey('DisciplineName', $discipline); - $this->assertArrayHasKey('SeqScr', $discipline); - $this->assertArrayHasKey('SeqRpt', $discipline); - - $currentKey = $this->buildTestSortKey($test); - if ($lastKey !== null) { - $this->assertGreaterThanOrEqual($lastKey, $currentKey, 'Tests are not ordered by discipline/test sequence'); - } - $lastKey = $currentKey; - } - } - - private function createOrderTestPatient(): int - { - $faker = Factory::create('id_ID'); - $patientPayload = [ - "PatientID" => "ORD" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000), - "AlternatePID" => "DMY" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000), - "Prefix" => $faker->title, - "NameFirst" => "Order", - "NameMiddle" => $faker->firstName, - "NameMaiden" => $faker->firstName, - "NameLast" => "Test", - "Suffix" => "S.Kom", - "NameAlias" => $faker->userName, - "Sex" => $faker->numberBetween(5, 6), - "PlaceOfBirth" => $faker->city, - "Birthdate" => "1990-01-01", - "ZIP" => $faker->postcode, - "Street_1" => $faker->streetAddress, - "Street_2" => "RT " . $faker->numberBetween(1, 10) . " RW " . $faker->numberBetween(1, 10), - "Street_3" => "Blok " . $faker->numberBetween(1, 20), - "City" => $faker->city, - "Province" => $faker->state, - "EmailAddress1" => "A" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000).'@gmail.com', - "EmailAddress2" => "B" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000).'@gmail.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", - "DeathIndicator" => (string) $faker->numberBetween(16, 17), - "LinkTo" => (string) $faker->numberBetween(2, 3), - "Custodian" => 1, - "PatIdt" => [ - "IdentifierType" => "KTP", - "Identifier" => $faker->nik() ?? $faker->numerify('################') - ], - "PatAtt" => [ - [ "Address" => "/api/upload/" . $faker->uuid . ".jpg" ] - ], - "PatCom" => $faker->sentence, - ]; - - if ($patientPayload['DeathIndicator'] == '16') { - $patientPayload['DeathDateTime'] = $faker->date('Y-m-d H:i:s'); - } else { - $patientPayload['DeathDateTime'] = null; - } - - $patientModel = new \App\Models\Patient\PatientModel(); - $internalPID = $patientModel->createPatient($patientPayload); - $this->assertNotNull($internalPID, 'Failed to create test patient. Response: ' . print_r($patientPayload, true)); - - return $internalPID; - } - - private function createOrderWithTests(int $internalPID, array $testSiteIDs, string $priority = 'R'): string - { - $payload = [ - 'InternalPID' => $internalPID, - 'Priority' => $priority, - 'Tests' => array_map(fn ($testSiteID) => ['TestSiteID' => $testSiteID], $testSiteIDs), - ]; - - $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $result->assertStatus(201); - - $body = json_decode($result->getBody(), true); - $this->assertEquals('success', $body['status']); - $orderID = $body['data']['OrderID'] ?? null; - $this->assertNotNull($orderID, 'Order creation response is missing OrderID'); - - return $orderID; - } - - private function collectTestSiteIDs(int $count = 2): array - { - $response = $this->call('get', 'api/test'); - $body = json_decode($response->getBody(), true); - $availableTests = $body['data'] ?? []; - - if (count($availableTests) < $count) { - $this->markTestSkipped('Need at least ' . $count . ' tests to validate ordering.'); - } - - $ids = array_values(array_filter(array_column($availableTests, 'TestSiteID'))); - return array_slice($ids, 0, $count); - } - - private function buildTestSortKey(array $test): string - { - $discipline = $test['Discipline'] ?? []; - $discSeqScr = $this->normalizeSequenceValue($discipline['SeqScr'] ?? null); - $discSeqRpt = $this->normalizeSequenceValue($discipline['SeqRpt'] ?? null); - $testSeqScr = $this->normalizeSequenceValue($test['SeqScr'] ?? null); - $testSeqRpt = $this->normalizeSequenceValue($test['SeqRpt'] ?? null); - $resultID = isset($test['ResultID']) ? (int)$test['ResultID'] : 0; - - return sprintf('%06d-%06d-%06d-%06d-%010d', $discSeqScr, $discSeqRpt, $testSeqScr, $testSeqRpt, $resultID); - } - - private function normalizeSequenceValue($value): int - { - if (is_numeric($value)) { - return (int)$value; - } - - return 999999; - } -} diff --git a/tests/feature/Organization/CodingSysControllerTest.php b/tests/feature/Organization/CodingSysControllerTest.php index 7f9d4a2..c1faa1f 100644 --- a/tests/feature/Organization/CodingSysControllerTest.php +++ b/tests/feature/Organization/CodingSysControllerTest.php @@ -20,8 +20,8 @@ class CodingSysControllerTest extends CIUnitTestCase public function testCreateCodingSys() { $payload = [ - 'CodingSysAbb' => 'ICD10', - 'FullText' => 'International Classification of Diseases 10', + 'CodingSysAbb' => 'ICD' . substr(time(), -3), + 'FullText' => 'International Classification of Diseases 10 ' . time(), 'Description' => 'Medical diagnosis coding system' ]; diff --git a/tests/feature/Organization/HostAppControllerTest.php b/tests/feature/Organization/HostAppControllerTest.php index 61a9820..90fe07e 100644 --- a/tests/feature/Organization/HostAppControllerTest.php +++ b/tests/feature/Organization/HostAppControllerTest.php @@ -11,6 +11,11 @@ class HostAppControllerTest extends CIUnitTestCase protected $endpoint = 'api/organization/hostapp'; + protected function setUp(): void + { + parent::setUp(); + } + public function testIndexHostApp() { $result = $this->get($this->endpoint); diff --git a/tests/feature/OrganizationControllerTest.php b/tests/feature/OrganizationControllerTest.php index c158db4..78ff00e 100644 --- a/tests/feature/OrganizationControllerTest.php +++ b/tests/feature/OrganizationControllerTest.php @@ -15,8 +15,8 @@ class OrganizationControllerTest extends CIUnitTestCase protected function setUp(): void { parent::setUp(); - - // Generate JWT Token + + // Generate JWT Token $key = getenv('JWT_SECRET') ?: 'my-secret-key'; $payload = [ 'iss' => 'localhost', @@ -27,8 +27,8 @@ class OrganizationControllerTest extends CIUnitTestCase 'uid' => 1, 'email' => 'admin@admin.com' ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } + $this->token = JWT::encode($payload, $key, 'HS256'); + } protected function callProtected($method, $path, $params = []) { diff --git a/tests/feature/PatVisit/PatVisitByPatientTest.php b/tests/feature/PatVisit/PatVisitByPatientTest.php index 1db6593..892fe60 100644 --- a/tests/feature/PatVisit/PatVisitByPatientTest.php +++ b/tests/feature/PatVisit/PatVisitByPatientTest.php @@ -5,11 +5,16 @@ namespace Tests\Feature\PatVisit; use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\CIUnitTestCase; -class PatVisitByPatientTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patvisit/patient'; +class PatVisitByPatientTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + protected $endpoint = 'api/patvisit/patient'; + + protected function setUp(): void + { + parent::setUp(); + } /** * Test: Show all visits by valid InternalPID diff --git a/tests/feature/PatVisit/PatVisitCreateTest.php b/tests/feature/PatVisit/PatVisitCreateTest.php index b00c1a0..b9fd0a4 100644 --- a/tests/feature/PatVisit/PatVisitCreateTest.php +++ b/tests/feature/PatVisit/PatVisitCreateTest.php @@ -2,22 +2,29 @@ namespace Tests\Feature\PatVisit; -use CodeIgniter\Test\FeatureTestTrait; -use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\FeatureTestTrait; +use CodeIgniter\Test\CIUnitTestCase; +use Tests\Support\Traits\CreatesPatients; -class PatVisitCreateTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patvisit'; +class PatVisitCreateTest extends CIUnitTestCase +{ + use FeatureTestTrait; + use CreatesPatients; + + protected $endpoint = 'api/patvisit'; + + protected function setUp(): void + { + parent::setUp(); + } /** * Test: Create patient visit with valid data */ public function testCreatePatientVisitSuccess() { - $payload = [ - "InternalPID"=> "1", + $payload = [ + "InternalPID"=> $this->createTestPatient(), "EpisodeID"=> null, "PatDiag"=> [ "DiagCode"=> null, diff --git a/tests/feature/PatVisit/PatVisitDeleteTest.php b/tests/feature/PatVisit/PatVisitDeleteTest.php index 80d7ebd..f54a194 100644 --- a/tests/feature/PatVisit/PatVisitDeleteTest.php +++ b/tests/feature/PatVisit/PatVisitDeleteTest.php @@ -2,14 +2,21 @@ namespace Tests\Feature\PatVisit; -use CodeIgniter\Test\FeatureTestTrait; -use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\FeatureTestTrait; +use CodeIgniter\Test\CIUnitTestCase; +use Tests\Support\Traits\CreatesPatients; -class PatVisitDeleteTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patvisit'; +class PatVisitDeleteTest extends CIUnitTestCase +{ + use FeatureTestTrait; + use CreatesPatients; + + protected $endpoint = 'api/patvisit'; + + protected function setUp(): void + { + parent::setUp(); + } /** * Test: Delete patient visit successfully (soft delete) @@ -17,8 +24,8 @@ class PatVisitDeleteTest extends CIUnitTestCase public function testDeletePatientVisitSuccess() { // Create a visit first to delete - $createPayload = [ - "InternalPID"=> "1", + $createPayload = [ + "InternalPID"=> $this->createTestPatient(), "EpisodeID"=> "TEST001", "PatVisitADT"=> [ "ADTCode"=> "A01", diff --git a/tests/feature/PatVisit/PatVisitShowTest.php b/tests/feature/PatVisit/PatVisitShowTest.php index 7c55764..fd42599 100644 --- a/tests/feature/PatVisit/PatVisitShowTest.php +++ b/tests/feature/PatVisit/PatVisitShowTest.php @@ -5,11 +5,16 @@ namespace Tests\Feature\PatVisit; use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\CIUnitTestCase; -class PatVisitShowTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patvisit'; +class PatVisitShowTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + protected $endpoint = 'api/patvisit'; + + protected function setUp(): void + { + parent::setUp(); + } public function testShowPatientVisitSuccess() { diff --git a/tests/feature/PatVisit/PatVisitUpdateTest.php b/tests/feature/PatVisit/PatVisitUpdateTest.php index ee10171..ae01b98 100644 --- a/tests/feature/PatVisit/PatVisitUpdateTest.php +++ b/tests/feature/PatVisit/PatVisitUpdateTest.php @@ -2,13 +2,20 @@ namespace Tests\Feature\PatVisit; -use CodeIgniter\Test\FeatureTestTrait; -use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\FeatureTestTrait; +use CodeIgniter\Test\CIUnitTestCase; +use Tests\Support\Traits\CreatesPatients; -class PatVisitUpdateTest extends CIUnitTestCase -{ - use FeatureTestTrait; - protected $endpoint = 'api/patvisit'; +class PatVisitUpdateTest extends CIUnitTestCase +{ + use FeatureTestTrait; + use CreatesPatients; + protected $endpoint = 'api/patvisit'; + + protected function setUp(): void + { + parent::setUp(); + } /** * Test: Update patient visit successfully @@ -16,8 +23,8 @@ class PatVisitUpdateTest extends CIUnitTestCase public function testUpdatePatientVisitSuccess() { // First create a visit to update - $createPayload = [ - "InternalPID"=> "1", + $createPayload = [ + "InternalPID"=> $this->createTestPatient(), "EpisodeID"=> "TEST001", "PatVisitADT"=> [ "ADTCode"=> "A01", @@ -33,12 +40,11 @@ class PatVisitUpdateTest extends CIUnitTestCase $pvid = $createJson['data']['PVID']; // Now update it - $payload = [ - 'InternalPVID' => $internalPVID, - 'PVID' => $pvid, - 'EpisodeID' => 'EPI001', - 'PatDiag' => [ - 'DiagCode' => 'A02', + $payload = [ + 'PVID' => $pvid, + 'EpisodeID' => 'EPI001', + 'PatDiag' => [ + 'DiagCode' => 'A02', 'DiagName' => 'Dysentery' ], 'PatVisitADT' => [ @@ -47,7 +53,7 @@ class PatVisitUpdateTest extends CIUnitTestCase ] ]; - $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/' . $internalPVID, $payload); // Pastikan response sukses (200 OK untuk update) $response->assertStatus(200); @@ -67,22 +73,22 @@ class PatVisitUpdateTest extends CIUnitTestCase /** * Test: Update patient visit with missing ID */ - public function testUpdatePatientVisitMissingId() - { - // InternalPVID tidak ada - $payload = [ - 'EpisodeID' => 'EPI002', - 'PatDiag' => ['DiagCode' => 'B01', 'DiagName' => 'Flu'] - ]; - - $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); - // Karena ID tidak ada → 400 Bad Request - $response->assertStatus(400); - - $response->assertJSONFragment([ - 'status' => 'error', - 'message' => 'Invalid or missing ID' - ]); + public function testUpdatePatientVisitMissingId() + { + // InternalPVID tidak ada + $payload = [ + 'EpisodeID' => 'EPI002', + 'PatDiag' => ['DiagCode' => 'B01', 'DiagName' => 'Flu'] + ]; + + $response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/0', $payload); + // Karena ID tidak valid → 400 Bad Request + $response->assertStatus(400); + + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Invalid or missing ID' + ]); } /** @@ -90,21 +96,20 @@ class PatVisitUpdateTest extends CIUnitTestCase */ public function testUpdatePatientVisitNotFound() { - $payload = [ - 'InternalPVID' => 999999, // Non-existent visit - 'EpisodeID' => 'EPI001', - 'PatDiag' => [ - 'DiagCode' => 'A02', - 'DiagName' => 'Dysentery' - ] - ]; - - $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); - $response->assertStatus(404); - $response->assertJSONFragment([ - 'status' => 'error', - 'message' => 'Visit not found' - ]); + $payload = [ + 'EpisodeID' => 'EPI001', + 'PatDiag' => [ + 'DiagCode' => 'A02', + 'DiagName' => 'Dysentery' + ] + ]; + + $response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/999999', $payload); + $response->assertStatus(404); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Visit not found' + ]); } /** @@ -112,12 +117,11 @@ class PatVisitUpdateTest extends CIUnitTestCase */ public function testUpdatePatientVisitInvalidId() { - $payload = [ - 'InternalPVID' => 'invalid', - 'PVID' => 'DV0001', - 'EpisodeID' => 'EPI001', - 'PatDiag' => [ - 'DiagCode' => 'A02', + $payload = [ + 'PVID' => 'DV0001', + 'EpisodeID' => 'EPI001', + 'PatDiag' => [ + 'DiagCode' => 'A02', 'DiagName' => 'Dysentery' ], 'PatVisitADT' => [ @@ -126,27 +130,27 @@ class PatVisitUpdateTest extends CIUnitTestCase ] ]; - $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); - $response->assertStatus(400); - $response->assertJSONFragment([ - 'status' => 'error', - 'message' => 'Invalid or missing ID' - ]); + $response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/invalid', $payload); + $response->assertStatus(400); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Invalid or missing ID' + ]); } /** * Test: Update patient visit with empty payload */ - public function testUpdatePatientVisitInvalidInput() - { - $payload = []; - - $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); - $response->assertStatus(400); - $response->assertJSONFragment([ - 'status' => 'error', - 'message' => 'Invalid or missing ID' - ]); - } + public function testUpdatePatientVisitInvalidInput() + { + $payload = []; + + $response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/0', $payload); + $response->assertStatus(400); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Invalid or missing ID' + ]); + } } diff --git a/tests/feature/Patients/PatientCheckTest.php b/tests/feature/Patients/PatientCheckTest.php index ac9d201..e88a430 100644 --- a/tests/feature/Patients/PatientCheckTest.php +++ b/tests/feature/Patients/PatientCheckTest.php @@ -23,11 +23,16 @@ use Faker\Factory; * 5. testCheckWithoutParams - Check without any parameters * 6. testCheckWithBothParams - Check with both parameters (PatientID takes priority) */ -class PatientCheckTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patient/check'; +class PatientCheckTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + protected $endpoint = 'api/patient/check'; + + protected function setUp(): void + { + parent::setUp(); + } /** * Test Case 1: Check existing PatientID diff --git a/tests/feature/Patients/PatientCreateTest.php b/tests/feature/Patients/PatientCreateTest.php index ed9f7f4..67b279f 100644 --- a/tests/feature/Patients/PatientCreateTest.php +++ b/tests/feature/Patients/PatientCreateTest.php @@ -6,10 +6,15 @@ use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\CIUnitTestCase; use Faker\Factory; -class PatientCreateTest extends CIUnitTestCase -{ - use FeatureTestTrait; - protected $endpoint = 'api/patient'; +class PatientCreateTest extends CIUnitTestCase +{ + use FeatureTestTrait; + protected $endpoint = 'api/patient'; + + protected function setUp(): void + { + parent::setUp(); + } // 400 - Passed // Validation Gagal - Array Tidak Complete diff --git a/tests/feature/Patients/PatientDeleteTest.php b/tests/feature/Patients/PatientDeleteTest.php index b02adad..40ce304 100644 --- a/tests/feature/Patients/PatientDeleteTest.php +++ b/tests/feature/Patients/PatientDeleteTest.php @@ -22,11 +22,16 @@ use Faker\Factory; * 7. testDeleteSQLInjectionAttempt - 400 error when SQL injection attempted * 8. testDeleteSuccess - 200 success when valid delete (commented - requires DB setup) */ -class PatientDeleteTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patient'; +class PatientDeleteTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + protected $endpoint = 'api/patient'; + + protected function setUp(): void + { + parent::setUp(); + } /** * Test Case 1: Delete without InternalPID key diff --git a/tests/feature/Patients/PatientIndexTest.php b/tests/feature/Patients/PatientIndexTest.php index cd77af5..55c1d1e 100644 --- a/tests/feature/Patients/PatientIndexTest.php +++ b/tests/feature/Patients/PatientIndexTest.php @@ -5,11 +5,16 @@ namespace Tests\Feature\Patients; use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\CIUnitTestCase; -class PatientIndexTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patient'; +class PatientIndexTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + protected $endpoint = 'api/patient'; + + protected function setUp(): void + { + parent::setUp(); + } /** * Case 1: tanpa parameter, harus 200 dan status success - Passed diff --git a/tests/feature/Patients/PatientShowTest.php b/tests/feature/Patients/PatientShowTest.php index 416b2a1..366e08a 100644 --- a/tests/feature/Patients/PatientShowTest.php +++ b/tests/feature/Patients/PatientShowTest.php @@ -5,11 +5,16 @@ namespace Tests\Feature\Patients; use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\CIUnitTestCase; -class PatientShowTest extends CIUnitTestCase -{ - use FeatureTestTrait; - - protected $endpoint = 'api/patient'; +class PatientShowTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + protected $endpoint = 'api/patient'; + + protected function setUp(): void + { + parent::setUp(); + } // 200 ok found - Passed public function testShowSingleRow() { @@ -81,4 +86,4 @@ class PatientShowTest extends CIUnitTestCase $this->assertGreaterThanOrEqual(1, count($data['data']['PatAtt'])); } -} \ No newline at end of file +} diff --git a/tests/feature/Patients/PatientUpdateTest.php b/tests/feature/Patients/PatientUpdateTest.php index 05809b5..d7ffd14 100644 --- a/tests/feature/Patients/PatientUpdateTest.php +++ b/tests/feature/Patients/PatientUpdateTest.php @@ -6,18 +6,23 @@ use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\CIUnitTestCase; use Faker\Factory; -class PatientUpdateTest extends CIUnitTestCase -{ - use FeatureTestTrait; - protected $endpoint = 'api/patient'; +class PatientUpdateTest extends CIUnitTestCase +{ + use FeatureTestTrait; + protected $endpoint = 'api/patient'; + + protected function setUp(): void + { + parent::setUp(); + } /** * 400 - Validation Fail * Coba update tanpa field wajib → harus gagal validasi. */ public function testUpdatePatientValidationFail() { - $payload = [ 'InternalPID' => null, 'NameFirst' => '' ]; // Tidak valid - $result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $payload = [ 'InternalPID' => null, 'NameFirst' => '' ]; // Tidak valid + $result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload); $result->assertStatus(400); $json = $result->getJSON(); @@ -33,13 +38,12 @@ class PatientUpdateTest extends CIUnitTestCase { $faker = Factory::create('id_ID'); - $payload = [ - 'InternalPID' => 999999, // Asumsi tidak ada di DB - "PatientID" => "SMAJ1", - "EmailAddress1" => 'asaas7890@gmail.com', - "Phone" => $faker->numerify('08##########'), - "MobilePhone" => $faker->numerify('08##########'), - 'NameFirst' => $faker->firstName, + $payload = [ + "PatientID" => "SMAJ1", + "EmailAddress1" => 'asaas7890@gmail.com', + "Phone" => $faker->numerify('08##########'), + "MobilePhone" => $faker->numerify('08##########'), + 'NameFirst' => $faker->firstName, 'NameLast' => $faker->lastName, 'Sex' => '1', 'Birthdate' => $faker->date('Y-m-d'), @@ -50,7 +54,7 @@ class PatientUpdateTest extends CIUnitTestCase ], ]; - $result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/999999', $payload); $result->assertStatus(201); // Update returns success even if no rows found (depending on logic) } @@ -65,12 +69,11 @@ class PatientUpdateTest extends CIUnitTestCase // NOTE: Sebaiknya ambil InternalPID yang sudah ada (mock atau dari DB fixture) // Untuk contoh ini kita asumsikan ada ID 1 - $payload = [ - 'InternalPID' => 1, - "PatientID" => "SMAJ1", - 'NameFirst' => $faker->firstName, - 'NameMiddle' => $faker->firstName, - 'NameLast' => $faker->lastName, + $payload = [ + "PatientID" => "SMAJ1", + 'NameFirst' => $faker->firstName, + 'NameMiddle' => $faker->firstName, + 'NameLast' => $faker->lastName, 'Sex' => '1', 'Birthdate' => $faker->date('Y-m-d'), 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', @@ -95,7 +98,7 @@ class PatientUpdateTest extends CIUnitTestCase $payload['DeathDateTime'] = null; } - $result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload); $result->assertStatus(201); $json = $result->getJSON(); $data = json_decode($json, true); @@ -109,12 +112,11 @@ class PatientUpdateTest extends CIUnitTestCase { $faker = Factory::create('id_ID'); - $payload = [ - 'InternalPID' => 1, - "PatientID" => "SMAJ1", - 'NameFirst' => $faker->firstName, - 'NameMiddle' => $faker->firstName, - 'NameLast' => $faker->lastName, + $payload = [ + "PatientID" => "SMAJ1", + 'NameFirst' => $faker->firstName, + 'NameMiddle' => $faker->firstName, + 'NameLast' => $faker->lastName, 'Sex' => '1', 'Birthdate' => $faker->date('Y-m-d'), 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', @@ -138,7 +140,7 @@ class PatientUpdateTest extends CIUnitTestCase $payload['DeathDateTime'] = null; } - $result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload); $result->assertStatus(201); } @@ -149,12 +151,11 @@ class PatientUpdateTest extends CIUnitTestCase { $faker = Factory::create('id_ID'); - $payload = [ - 'InternalPID' => 1, - "PatientID" => "SMAJ1", - 'NameFirst' => $faker->firstName, - 'NameMiddle' => $faker->firstName, - 'NameLast' => $faker->lastName, + $payload = [ + "PatientID" => "SMAJ1", + 'NameFirst' => $faker->firstName, + 'NameMiddle' => $faker->firstName, + 'NameLast' => $faker->lastName, 'Sex' => '1', 'Birthdate' => $faker->date('Y-m-d'), 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', @@ -176,7 +177,7 @@ class PatientUpdateTest extends CIUnitTestCase $payload['DeathDateTime'] = null; } - $result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload); $result->assertStatus(201); } @@ -187,12 +188,11 @@ class PatientUpdateTest extends CIUnitTestCase { $faker = Factory::create('id_ID'); - $payload = [ - 'InternalPID' => 1, - "PatientID" => "SMAJ1", - 'NameFirst' => $faker->firstName, - 'NameMiddle' => $faker->firstName, - 'NameLast' => $faker->lastName, + $payload = [ + "PatientID" => "SMAJ1", + 'NameFirst' => $faker->firstName, + 'NameMiddle' => $faker->firstName, + 'NameLast' => $faker->lastName, 'Sex' => '1', 'Birthdate' => $faker->date('Y-m-d'), 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', @@ -216,7 +216,7 @@ class PatientUpdateTest extends CIUnitTestCase $payload['DeathDateTime'] = null; } - $result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload); $result->assertStatus(201); } @@ -227,12 +227,11 @@ class PatientUpdateTest extends CIUnitTestCase { $faker = Factory::create('id_ID'); - $payload = [ - 'InternalPID' => 1, - "PatientID" => "SMAJ1", - 'NameFirst' => $faker->firstName, - 'NameMiddle' => $faker->firstName, - 'NameLast' => $faker->lastName, + $payload = [ + "PatientID" => "SMAJ1", + 'NameFirst' => $faker->firstName, + 'NameMiddle' => $faker->firstName, + 'NameLast' => $faker->lastName, 'Sex' => '1', 'Birthdate' => $faker->date('Y-m-d'), 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', @@ -256,7 +255,7 @@ class PatientUpdateTest extends CIUnitTestCase $payload['DeathDateTime'] = null; } - $result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload); $result->assertStatus(500); $json = $result->getJSON(); diff --git a/tests/feature/SimpleTest.php b/tests/feature/SimpleTest.php deleted file mode 100644 index cbc9e6c..0000000 --- a/tests/feature/SimpleTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertTrue(true); - } -} diff --git a/tests/feature/Test/TestCreateVariantsTest.php b/tests/feature/Test/TestCreateVariantsTest.php new file mode 100644 index 0000000..95a2e42 --- /dev/null +++ b/tests/feature/Test/TestCreateVariantsTest.php @@ -0,0 +1,443 @@ +testModel = new TestDefSiteModel(); + } + + public function testCreateTechnicalWithoutReferenceOrTestMap(): void + { + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type); + } + } + + public function testCreateTechnicalWithNumericReference(): void + { + $refnum = $this->buildRefNumEntries('NMRC', true); + foreach (['TEST', 'PARAM'] as $type) { + $this->assertTechnicalCreated($type, [ + 'ResultType' => 'NMRIC', + 'RefType' => 'RANGE', + ], $refnum); + } + } + + 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 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']); + } + + 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']); + } + + 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, + 'VisibleScr' => 1, + 'VisibleRpt' => 1, + 'CountStat' => 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, + 'VisibleScr' => 1, + 'VisibleRpt' => 1, + 'CountStat' => 0, + 'details' => [ + 'DisciplineID' => 2, + 'DepartmentID' => 2, + 'FormulaCode' => '{GLU} + {CREA}', + 'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members), + ], + ]; + + 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, + 'VisibleScr' => 1, + 'VisibleRpt' => 1, + 'CountStat' => 1, + 'details' => [ + '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' => $details['Decimal'] ?? 0, + ]; + + foreach (['ResultType', 'RefType', 'FormulaCode', 'members', 'ExpectedTAT'] 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' => 120, + 'Flag' => 'N', + 'Interpretation' => 'Normal range', + ], + ]; + + if ($multiple) { + $entries[] = [ + 'NumRefType' => $numRefType, + 'RangeType' => $rangeType, + 'Sex' => '1', + 'LowSign' => '>', + 'Low' => 5, + 'HighSign' => '<', + 'High' => $numRefType === 'THOLD' ? 50 : 15, + 'AgeStart' => 0, + 'AgeEnd' => 99, + 'Flag' => 'N', + 'Interpretation' => 'Alternate range', + ]; + } + + return $entries; + } + + private function buildRefTxtEntries(string $txtRefType, bool $multiple = false): array + { + $entries = [ + [ + 'SpcType' => 'GEN', + 'TxtRefType' => $txtRefType, + 'Sex' => '2', + 'AgeStart' => 0, + 'AgeEnd' => 120, + 'RefTxt' => $txtRefType === 'VSET' ? 'NORM=Normal;ABN=Abnormal' : 'NORM=Normal', + 'Flag' => 'N', + ], + ]; + + if ($multiple) { + $entries[] = [ + 'SpcType' => 'GEN', + 'TxtRefType' => $txtRefType, + 'Sex' => '1', + 'AgeStart' => 0, + 'AgeEnd' => 120, + '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 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/TestsControllerTest.php b/tests/feature/TestsControllerTest.php deleted file mode 100644 index 2fbb2d2..0000000 --- a/tests/feature/TestsControllerTest.php +++ /dev/null @@ -1,417 +0,0 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com' - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - protected function callProtected($method, $path, $params = []) - { - return $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->call($method, $path, $params); - } - - public function testIndexReturnsSuccess() - { - $result = $this->callProtected('get', 'api/tests'); - - $result->assertStatus(200); - $json = $result->getJSON(); - $data = json_decode($json, true); - - $this->assertEquals('success', $data['status']); - $this->assertIsArray($data['data']); - } - - public function testShowReturnsDataIfFound() - { - // First get an ID - $indexResult = $this->callProtected('get', 'api/tests'); - $indexData = json_decode($indexResult->getJSON(), true); - - if (empty($indexData['data'])) { - $this->markTestSkipped('No test definitions found in database to test show.'); - } - - $id = $indexData['data'][0]['TestSiteID']; - $result = $this->callProtected('get', "api/tests/$id"); - - $result->assertStatus(200); - $json = $result->getJSON(); - $data = json_decode($json, true); - - $this->assertEquals('success', $data['status']); - $this->assertIsArray($data['data']); - $this->assertEquals($id, $data['data']['TestSiteID']); - } - - public function testCreateTestWithThreshold() - { - $testData = [ - 'TestSiteCode' => 'TH' . substr(time(), -4), - 'TestSiteName' => 'Threshold Test ' . time(), - 'TestType' => 'TEST', - 'SiteID' => 1, - 'details' => [ - 'RefType' => 'THOLD', - 'ResultType' => 'NMRIC' - ], - 'refnum' => [ - [ - 'NumRefType' => 'THOLD', - 'RangeType' => 'VALUE', - 'Sex' => '1', - 'AgeStart' => 0, - 'AgeEnd' => 100, - 'LowSign' => '>', - 'Low' => 5.5, - 'Interpretation' => 'High' - ] - ] - ]; - - $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->withBody(json_encode($testData)) - ->call('post', 'api/tests'); - - $result->assertStatus(201); - $json = $result->getJSON(); - $data = json_decode($json, true); - - $this->assertEquals('created', $data['status']); - $id = $data['data']['TestSiteId']; - - // Verify retrieval - $showResult = $this->callProtected('get', "api/tests/$id"); - $showData = json_decode($showResult->getJSON(), true); - - $this->assertArrayHasKey('refnum', $showData['data']); - $this->assertCount(1, $showData['data']['refnum']); - $this->assertEquals(5.5, $showData['data']['refnum'][0]['Low']); - $this->assertEquals('High', $showData['data']['refnum'][0]['Interpretation']); - } - - /** - * Test valid TestType and ResultType combinations - * @dataProvider validTestTypeResultTypeProvider - */ - public function testValidTestTypeResultTypeCombinations($testType, $resultType, $refType, $shouldSucceed) - { - $testData = [ - 'TestSiteCode' => 'TT' . substr(time(), -4) . rand(10, 99), - 'TestSiteName' => 'Type Test ' . time(), - 'TestType' => $testType, - 'SiteID' => 1, - 'details' => [ - 'ResultType' => $resultType, - 'RefType' => $refType - ] - ]; - - // Add reference data if needed - if ($refType === 'RANGE' || $refType === 'THOLD') { - $testData['refnum'] = [ - [ - 'NumRefType' => $refType, - 'RangeType' => 'VALUE', - 'Sex' => '1', - 'AgeStart' => 0, - 'AgeEnd' => 100, - 'LowSign' => '>', - 'Low' => 5.5, - 'Interpretation' => 'Normal' - ] - ]; - } elseif ($refType === 'VSET' || $refType === 'TEXT') { - $testData['reftxt'] = [ - [ - 'TxtRefType' => $refType, - 'Sex' => '1', - 'AgeStart' => 0, - 'AgeEnd' => 100, - 'RefTxt' => 'Normal range text', - 'Flag' => 'N' - ] - ]; - } - - $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->withBody(json_encode($testData)) - ->call('post', 'api/tests'); - - if ($shouldSucceed) { - $result->assertStatus(201); - $data = json_decode($result->getJSON(), true); - $this->assertEquals('created', $data['status']); - } else { - // Invalid combinations should fail validation or return error - $this->assertGreaterThanOrEqual(400, $result->getStatusCode()); - } - } - - public function validTestTypeResultTypeProvider() - { - return [ - // TEST type - can have NMRIC, RANGE, TEXT, VSET - 'TEST with NMRIC' => ['TEST', 'NMRIC', 'RANGE', true], - 'TEST with RANGE' => ['TEST', 'RANGE', 'RANGE', true], - 'TEST with TEXT' => ['TEST', 'TEXT', 'TEXT', true], - 'TEST with VSET' => ['TEST', 'VSET', 'VSET', true], - 'TEST with THOLD' => ['TEST', 'NMRIC', 'THOLD', true], - - // PARAM type - can have NMRIC, RANGE, TEXT, VSET - 'PARAM with NMRIC' => ['PARAM', 'NMRIC', 'RANGE', true], - 'PARAM with RANGE' => ['PARAM', 'RANGE', 'RANGE', true], - 'PARAM with TEXT' => ['PARAM', 'TEXT', 'TEXT', true], - 'PARAM with VSET' => ['PARAM', 'VSET', 'VSET', true], - - // CALC type - only NMRIC - 'CALC with NMRIC' => ['CALC', 'NMRIC', 'RANGE', true], - - // GROUP type - only NORES - 'GROUP with NORES' => ['GROUP', 'NORES', 'NOREF', true], - - // TITLE type - only NORES - 'TITLE with NORES' => ['TITLE', 'NORES', 'NOREF', true], - ]; - } - - /** - * Test ResultType to RefType mapping - * @dataProvider resultTypeToRefTypeProvider - */ - public function testResultTypeToRefTypeMapping($resultType, $refType, $expectedRefTable) - { - $testData = [ - 'TestSiteCode' => 'RT' . substr(time(), -4) . rand(10, 99), - 'TestSiteName' => 'RefType Test ' . time(), - 'TestType' => 'TEST', - 'SiteID' => 1, - 'details' => [ - 'ResultType' => $resultType, - 'RefType' => $refType - ] - ]; - - // Add appropriate reference data - if ($expectedRefTable === 'refnum') { - $testData['refnum'] = [ - [ - 'NumRefType' => $refType, - 'RangeType' => 'VALUE', - 'Sex' => '1', - 'AgeStart' => 18, - 'AgeEnd' => 99, - 'LowSign' => 'GE', - 'Low' => 10, - 'HighSign' => 'LE', - 'High' => 20, - 'Interpretation' => 'Normal' - ] - ]; - } elseif ($expectedRefTable === 'reftxt') { - $testData['reftxt'] = [ - [ - 'TxtRefType' => $refType, - 'Sex' => '1', - 'AgeStart' => 18, - 'AgeEnd' => 99, - 'RefTxt' => 'Reference text', - 'Flag' => 'N' - ] - ]; - } - - $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->withBody(json_encode($testData)) - ->call('post', 'api/tests'); - - $result->assertStatus(201); - $data = json_decode($result->getJSON(), true); - $id = $data['data']['TestSiteId']; - - // Verify the reference data was stored in correct table - $showResult = $this->callProtected('get', "api/tests/$id"); - $showData = json_decode($showResult->getJSON(), true); - - if ($expectedRefTable === 'refnum') { - $this->assertArrayHasKey('refnum', $showData['data']); - $this->assertNotEmpty($showData['data']['refnum']); - } elseif ($expectedRefTable === 'reftxt') { - $this->assertArrayHasKey('reftxt', $showData['data']); - $this->assertNotEmpty($showData['data']['reftxt']); - } - } - - public function resultTypeToRefTypeProvider() - { - return [ - // NMRIC with RANGE → refnum table - 'NMRIC with RANGE uses refnum' => ['NMRIC', 'RANGE', 'refnum'], - // NMRIC with THOLD → refnum table - 'NMRIC with THOLD uses refnum' => ['NMRIC', 'THOLD', 'refnum'], - // RANGE with RANGE → refnum table - 'RANGE with RANGE uses refnum' => ['RANGE', 'RANGE', 'refnum'], - // RANGE with THOLD → refnum table - 'RANGE with THOLD uses refnum' => ['RANGE', 'THOLD', 'refnum'], - // VSET with VSET → reftxt table - 'VSET with VSET uses reftxt' => ['VSET', 'VSET', 'reftxt'], - // TEXT with TEXT → reftxt table - 'TEXT with TEXT uses reftxt' => ['TEXT', 'TEXT', 'reftxt'], - ]; - } - - /** - * Test CALC type always has NMRIC result type - */ - public function testCalcTypeAlwaysHasNmricResultType() - { - $testData = [ - 'TestSiteCode' => 'CALC' . substr(time(), -4), - 'TestSiteName' => 'Calc Test ' . time(), - 'TestType' => 'CALC', - 'SiteID' => 1, - 'details' => [ - 'DisciplineID' => 1, - 'DepartmentID' => 1, - 'FormulaCode' => 'WEIGHT/(HEIGHT/100)^2', - 'Unit1' => 'kg/m2', - 'Decimal' => 1 - ], - 'members' => [] - ]; - - $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->withBody(json_encode($testData)) - ->call('post', 'api/tests'); - - $result->assertStatus(201); - $data = json_decode($result->getJSON(), true); - $id = $data['data']['TestSiteId']; - - // Verify CALC test was created - $showResult = $this->callProtected('get', "api/tests/$id"); - $showData = json_decode($showResult->getJSON(), true); - - $this->assertEquals('CALC', $showData['data']['TestType']); - $this->assertArrayHasKey('testdefcal', $showData['data']); - } - - /** - * Test GROUP type has no result (NORES) - */ - public function testGroupTypeHasNoResult() - { - // First create member tests - $member1Data = [ - 'TestSiteCode' => 'M1' . substr(time(), -4), - 'TestSiteName' => 'Member 1 ' . time(), - 'TestType' => 'TEST', - 'SiteID' => 1, - 'details' => [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE' - ], - 'refnum' => [ - [ - 'NumRefType' => 'RANGE', - 'RangeType' => 'VALUE', - 'Sex' => '1', - 'AgeStart' => 0, - 'AgeEnd' => 100, - 'LowSign' => '>', - 'Low' => 5.5, - 'Interpretation' => 'Normal' - ] - ] - ]; - - $result1 = $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->withBody(json_encode($member1Data)) - ->call('post', 'api/tests'); - $data1 = json_decode($result1->getJSON(), true); - $member1Id = $data1['data']['TestSiteId']; - - $member2Data = [ - 'TestSiteCode' => 'M2' . substr(time(), -4), - 'TestSiteName' => 'Member 2 ' . time(), - 'TestType' => 'TEST', - 'SiteID' => 1, - 'details' => [ - 'ResultType' => 'NMRIC', - 'RefType' => 'RANGE' - ], - 'refnum' => [ - [ - 'NumRefType' => 'RANGE', - 'RangeType' => 'VALUE', - 'Sex' => '1', - 'AgeStart' => 0, - 'AgeEnd' => 100, - 'LowSign' => '>', - 'Low' => 5.5, - 'Interpretation' => 'Normal' - ] - ] - ]; - - $result2 = $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->withBody(json_encode($member2Data)) - ->call('post', 'api/tests'); - $data2 = json_decode($result2->getJSON(), true); - $member2Id = $data2['data']['TestSiteId']; - - // Create group test - $groupData = [ - 'TestSiteCode' => 'GRP' . substr(time(), -4), - 'TestSiteName' => 'Group Test ' . time(), - 'TestType' => 'GROUP', - 'SiteID' => 1, - 'details' => [ - 'ResultType' => 'NORES', - 'RefType' => 'NOREF' - ], - 'members' => [$member1Id, $member2Id] - ]; - - $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) - ->withBody(json_encode($groupData)) - ->call('post', 'api/tests'); - - $result->assertStatus(201); - $data = json_decode($result->getJSON(), true); - $id = $data['data']['TestSiteId']; - - // Verify GROUP test was created with members - $showResult = $this->callProtected('get', "api/tests/$id"); - $showData = json_decode($showResult->getJSON(), true); - - $this->assertEquals('GROUP', $showData['data']['TestType']); - $this->assertArrayHasKey('testdefgrp', $showData['data']); - } -} diff --git a/tests/feature/UniformShowTest.php b/tests/feature/UniformShowTest.php deleted file mode 100644 index 5f0b0b3..0000000 --- a/tests/feature/UniformShowTest.php +++ /dev/null @@ -1,111 +0,0 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com' - ]; - $this->token = \Firebase\JWT\JWT::encode($payload, $key, 'HS256'); - } - - // Override get to inject cookie header - public function get(string $path, array $options = []) { - $this->withHeaders(['Cookie' => 'token=' . $this->token]); - return $this->call('get', $path, $options); - } - - /** - * Test that show endpoints return a single object (associative array) in 'data' when found. - */ - public function testShowEndpointsReturnObjectWhenFound() - { - // representative endpoints. - $endpoints = [ - 'api/location', - 'api/organization/site', - 'api/organization/account', - 'api/patient', - 'api/tests', - 'api/specimen/containerdef', - 'api/contact', - ]; - - foreach ($endpoints as $url) { - // We first check index to get a valid ID if possible - $indexResult = $this->get($url); - $indexBody = json_decode($indexResult->response()->getBody(), true); - - $id = 1; // logical default - if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) { - $firstItem = $indexBody['data'][0]; - // Try to guess ID key - $idKeys = ['LocationID', 'SiteID', 'AccountID', 'InternalPID', 'TestSiteID', 'ConDefID', 'ContactID', 'VID', 'id']; - foreach ($idKeys as $key) { - if (isset($firstItem[$key])) { - $id = $firstItem[$key]; - break; - } - } - } - - $showUrl = $url . '/' . $id; - $result = $this->get($showUrl); - $body = json_decode($result->response()->getBody(), true); - - if ($result->response()->getStatusCode() === 200 && isset($body['data']) && $body['data'] !== null) { - $this->assertTrue( - $this->is_assoc($body['data']), - "Endpoint $showUrl should return an object in 'data', but got a sequential array or empty array. Body: " . $result->response()->getBody() - ); - } - } - } - - public function testShowEndpointsReturnNullWhenNotFound() - { - $endpoints = [ - 'api/location/9999999', - 'api/organization/site/9999999', - 'api/patient/9999999', - ]; - - foreach ($endpoints as $url) { - $result = $this->get($url); - $result->assertStatus(200); - $body = json_decode($result->response()->getBody(), true); - - $this->assertArrayHasKey('data', $body, "Endpoint $url missing 'data' key. Body: " . $result->response()->getBody()); - $this->assertNull($body['data'], "Endpoint $url should return null in 'data' when not found. Body: " . $result->response()->getBody()); - } - } - - /** - * Helper to check if array is associative. - */ - private function is_assoc(array $arr) - { - if (array() === $arr) return false; - return array_keys($arr) !== range(0, count($arr) - 1); - } -} diff --git a/tests/feature/ValueSet/ValueSetControllerTest.php b/tests/feature/ValueSet/ValueSetControllerTest.php index 41f14bc..444a37b 100644 --- a/tests/feature/ValueSet/ValueSetControllerTest.php +++ b/tests/feature/ValueSet/ValueSetControllerTest.php @@ -76,8 +76,8 @@ class ValueSetControllerTest extends CIUnitTestCase $values = array_column($data['data'], 'value'); $labels = array_column($data['data'], 'label'); - $this->assertContains('1', $values); - $this->assertContains('2', $values); + $this->assertContains('F', $values); + $this->assertContains('M', $values); $this->assertContains('Female', $labels); $this->assertContains('Male', $labels); } diff --git a/tests/phpunit-bootstrap.php b/tests/phpunit-bootstrap.php new file mode 100644 index 0000000..ef88ffd --- /dev/null +++ b/tests/phpunit-bootstrap.php @@ -0,0 +1,46 @@ +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(); + } +} diff --git a/tests/unit/Rule/RuleDefModelTest.php b/tests/unit/Rule/RuleDefModelTest.php deleted file mode 100644 index d3a7e0b..0000000 --- a/tests/unit/Rule/RuleDefModelTest.php +++ /dev/null @@ -1,271 +0,0 @@ -model = new RuleDefModel(); - } - - /** - * Test that getActiveByEvent returns empty array when TestSiteID is null - * This ensures rules are standalone and must be explicitly included by test - */ - public function testGetActiveByEventReturnsEmptyWithoutTestSiteID(): void - { - $rules = $this->model->getActiveByEvent('test_created', null); - - $this->assertIsArray($rules); - $this->assertEmpty($rules); - } - - /** - * Test that a rule can be linked to multiple tests - */ - public function testRuleCanBeLinkedToMultipleTests(): void - { - $db = \Config\Database::connect(); - - // Get two existing tests - $tests = $db->table('testdefsite') - ->where('EndDate', null) - ->limit(2) - ->get() - ->getResultArray(); - - if (count($tests) < 2) { - $this->markTestSkipped('Need at least 2 tests available in testdefsite table'); - } - - $testSiteID1 = (int) $tests[0]['TestSiteID']; - $testSiteID2 = (int) $tests[1]['TestSiteID']; - - // Create a rule - $ruleData = [ - 'RuleCode' => 'MULTI_TEST_RULE', - 'RuleName' => 'Multi Test Rule', - 'EventCode' => 'test_created', - 'ConditionExpr' => 'order["InternalOID"] > 0', - 'CreateDate' => date('Y-m-d H:i:s'), - ]; - - $ruleID = $this->model->insert($ruleData, true); - $this->assertNotFalse($ruleID); - - // Link rule to both tests - $this->model->linkTest($ruleID, $testSiteID1); - $this->model->linkTest($ruleID, $testSiteID2); - - // Verify rule is returned for both test sites - $rules1 = $this->model->getActiveByEvent('test_created', $testSiteID1); - $this->assertNotEmpty($rules1); - $this->assertCount(1, $rules1); - $this->assertEquals($ruleID, $rules1[0]['RuleID']); - - $rules2 = $this->model->getActiveByEvent('test_created', $testSiteID2); - $this->assertNotEmpty($rules2); - $this->assertCount(1, $rules2); - $this->assertEquals($ruleID, $rules2[0]['RuleID']); - - // Cleanup - $this->model->delete($ruleID); - } - - /** - * Test that rules only work when explicitly linked to a test - */ - public function testRulesOnlyWorkWhenExplicitlyLinked(): void - { - $db = \Config\Database::connect(); - - // Get an existing test - $test = $db->table('testdefsite') - ->where('EndDate', null) - ->limit(1) - ->get() - ->getRowArray(); - - if (!$test) { - $this->markTestSkipped('No tests available in testdefsite table'); - } - - $testSiteID = (int) $test['TestSiteID']; - - // Create a rule (not linked to any test yet) - $ruleData = [ - 'RuleCode' => 'UNLINKED_RULE', - 'RuleName' => 'Unlinked Test Rule', - 'EventCode' => 'test_created', - 'ConditionExpr' => 'true', - 'CreateDate' => date('Y-m-d H:i:s'), - ]; - - $ruleID = $this->model->insert($ruleData, true); - $this->assertNotFalse($ruleID); - - // Verify rule is NOT returned when not linked - $rules = $this->model->getActiveByEvent('test_created', $testSiteID); - $this->assertEmpty($rules); - - // Now link the rule - $this->model->linkTest($ruleID, $testSiteID); - - // Verify rule is now returned - $rules = $this->model->getActiveByEvent('test_created', $testSiteID); - $this->assertNotEmpty($rules); - $this->assertCount(1, $rules); - - // Cleanup - $this->model->delete($ruleID); - } - - /** - * Test that unlinking a test removes the rule for that test - */ - public function testUnlinkingTestRemovesRule(): void - { - $db = \Config\Database::connect(); - - // Get two existing tests - $tests = $db->table('testdefsite') - ->where('EndDate', null) - ->limit(2) - ->get() - ->getResultArray(); - - if (count($tests) < 2) { - $this->markTestSkipped('Need at least 2 tests available in testdefsite table'); - } - - $testSiteID1 = (int) $tests[0]['TestSiteID']; - $testSiteID2 = (int) $tests[1]['TestSiteID']; - - // Create a rule and link to both tests - $ruleData = [ - 'RuleCode' => 'UNLINK_TEST', - 'RuleName' => 'Unlink Test Rule', - 'EventCode' => 'test_created', - 'ConditionExpr' => 'true', - 'CreateDate' => date('Y-m-d H:i:s'), - ]; - - $ruleID = $this->model->insert($ruleData, true); - $this->model->linkTest($ruleID, $testSiteID1); - $this->model->linkTest($ruleID, $testSiteID2); - - // Verify rule is returned for both - $this->assertNotEmpty($this->model->getActiveByEvent('test_created', $testSiteID1)); - $this->assertNotEmpty($this->model->getActiveByEvent('test_created', $testSiteID2)); - - // Unlink from first test - $this->model->unlinkTest($ruleID, $testSiteID1); - - // Verify rule is NOT returned for first test but still for second - $this->assertEmpty($this->model->getActiveByEvent('test_created', $testSiteID1)); - $this->assertNotEmpty($this->model->getActiveByEvent('test_created', $testSiteID2)); - - // Cleanup - $this->model->delete($ruleID); - } - - /** - * Test that deleted (soft deleted) rules are not returned - */ - public function testDeletedRulesAreNotReturned(): void - { - $db = \Config\Database::connect(); - - $test = $db->table('testdefsite') - ->where('EndDate', null) - ->limit(1) - ->get() - ->getRowArray(); - - if (!$test) { - $this->markTestSkipped('No tests available in testdefsite table'); - } - - $testSiteID = (int) $test['TestSiteID']; - - // Create a rule and link it - $ruleData = [ - 'RuleCode' => 'DELETED_RULE', - 'RuleName' => 'Deleted Test Rule', - 'EventCode' => 'test_created', - 'ConditionExpr' => 'true', - 'CreateDate' => date('Y-m-d H:i:s'), - ]; - - $ruleID = $this->model->insert($ruleData, true); - $this->assertNotFalse($ruleID); - $this->model->linkTest($ruleID, $testSiteID); - - // Verify rule is returned - $rules = $this->model->getActiveByEvent('test_created', $testSiteID); - $this->assertNotEmpty($rules); - - // Soft delete the rule - $this->model->delete($ruleID); - - // Verify deleted rule is NOT returned - $rules = $this->model->getActiveByEvent('test_created', $testSiteID); - $this->assertEmpty($rules); - } - - /** - * Test getting linked tests for a rule - */ - public function testGetLinkedTests(): void - { - $db = \Config\Database::connect(); - - // Get two existing tests - $tests = $db->table('testdefsite') - ->where('EndDate', null) - ->limit(2) - ->get() - ->getResultArray(); - - if (count($tests) < 2) { - $this->markTestSkipped('Need at least 2 tests available'); - } - - $testSiteID1 = (int) $tests[0]['TestSiteID']; - $testSiteID2 = (int) $tests[1]['TestSiteID']; - - // Create a rule - $ruleData = [ - 'RuleCode' => 'LINKED_TESTS', - 'RuleName' => 'Linked Tests Rule', - 'EventCode' => 'test_created', - 'ConditionExpr' => 'true', - 'CreateDate' => date('Y-m-d H:i:s'), - ]; - - $ruleID = $this->model->insert($ruleData, true); - $this->model->linkTest($ruleID, $testSiteID1); - $this->model->linkTest($ruleID, $testSiteID2); - - // Get linked tests - $linkedTests = $this->model->getLinkedTests($ruleID); - - $this->assertCount(2, $linkedTests); - $this->assertContains($testSiteID1, $linkedTests); - $this->assertContains($testSiteID2, $linkedTests); - - // Cleanup - $this->model->delete($ruleID); - } -} diff --git a/tests/unit/Rules/RuleEngineMultiActionTest.php b/tests/unit/Rules/RuleEngineMultiActionTest.php deleted file mode 100644 index b9036be..0000000 --- a/tests/unit/Rules/RuleEngineMultiActionTest.php +++ /dev/null @@ -1,355 +0,0 @@ -engine = new RuleEngineService(); - - // Seed test data - $this->seedTestData(); - } - - private function seedTestData(): void - { - $db = \Config\Database::connect(); - - // Insert test testdefsite - $db->table('testdefsite')->insert([ - 'TestSiteCode' => 'GLU', - 'TestSiteName' => 'Glucose', - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - $db->table('testdefsite')->insert([ - 'TestSiteCode' => 'HBA1C', - 'TestSiteName' => 'HbA1c', - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - // Insert test order - $db->table('orders')->insert([ - 'OrderID' => 'ORD001', - 'Sex' => 'M', - 'Age' => 45, - 'Priority' => 'R', - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - } - - public function testExecuteSetResult() - { - $db = \Config\Database::connect(); - - // Get order ID - $order = $db->table('orders')->where('OrderID', 'ORD001')->get()->getRowArray(); - $internalOID = $order['InternalOID'] ?? null; - - // Get test site ID - $testSite = $db->table('testdefsite')->where('TestSiteCode', 'GLU')->get()->getRowArray(); - $testSiteID = $testSite['TestSiteID'] ?? null; - - $this->assertNotNull($internalOID); - $this->assertNotNull($testSiteID); - - // Insert initial patres row - $db->table('patres')->insert([ - 'OrderID' => $internalOID, - 'TestSiteID' => $testSiteID, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - // Execute RESULT_SET action - $action = [ - 'type' => 'RESULT_SET', - 'value' => 5.5, - ]; - - $context = [ - 'order' => [ - 'InternalOID' => $internalOID, - 'Sex' => 'M', - 'Age' => 45, - ], - 'testSiteID' => $testSiteID, - ]; - - $this->invokeMethod($this->engine, 'executeAction', [$action, $context]); - - // Verify result was set - $patres = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->where('DelDate', null) - ->get()->getRowArray(); - - $this->assertNotNull($patres); - $this->assertEquals(5.5, $patres['Result']); - } - - public function testExecuteInsertTest() - { - $db = \Config\Database::connect(); - - // Get order ID - $order = $db->table('orders')->where('OrderID', 'ORD001')->get()->getRowArray(); - $internalOID = $order['InternalOID'] ?? null; - - $this->assertNotNull($internalOID); - - // Execute TEST_INSERT action - $action = [ - 'type' => 'TEST_INSERT', - 'testCode' => 'HBA1C', - ]; - - $context = [ - 'order' => [ - 'InternalOID' => $internalOID, - ], - ]; - - $this->invokeMethod($this->engine, 'executeAction', [$action, $context]); - - // Verify test was inserted - $testSite = $db->table('testdefsite')->where('TestSiteCode', 'HBA1C')->get()->getRowArray(); - $testSiteID = $testSite['TestSiteID'] ?? null; - - $patres = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->where('DelDate', null) - ->get()->getRowArray(); - - $this->assertNotNull($patres); - } - - public function testExecuteAddComment() - { - $db = \Config\Database::connect(); - - // Get order ID - $order = $db->table('orders')->where('OrderID', 'ORD001')->get()->getRowArray(); - $internalOID = $order['InternalOID'] ?? null; - - $this->assertNotNull($internalOID); - - // Execute COMMENT_INSERT action - $action = [ - 'type' => 'COMMENT_INSERT', - 'comment' => 'Test comment from rule engine', - ]; - - $context = [ - 'order' => [ - 'InternalOID' => $internalOID, - ], - ]; - - $this->invokeMethod($this->engine, 'executeAction', [$action, $context]); - - // Verify comment was added - $comment = $db->table('ordercom') - ->where('InternalOID', $internalOID) - ->where('Comment', 'Test comment from rule engine') - ->get()->getRowArray(); - - $this->assertNotNull($comment); - } - - public function testExecuteNoOp() - { - // Execute NO_OP action - should not throw or do anything - $action = [ - 'type' => 'NO_OP', - ]; - - $context = [ - 'order' => [ - 'InternalOID' => 1, - ], - ]; - - // Should not throw exception - $this->invokeMethod($this->engine, 'executeAction', [$action, $context]); - - // Test passes if no exception is thrown - $this->assertTrue(true); - } - - public function testMultiActionExecution() - { - $db = \Config\Database::connect(); - - // Get order ID - $order = $db->table('orders')->where('OrderID', 'ORD001')->get()->getRowArray(); - $internalOID = $order['InternalOID'] ?? null; - - // Get test site ID - $testSite = $db->table('testdefsite')->where('TestSiteCode', 'GLU')->get()->getRowArray(); - $testSiteID = $testSite['TestSiteID'] ?? null; - - $this->assertNotNull($internalOID); - $this->assertNotNull($testSiteID); - - // Insert initial patres row - $db->table('patres')->insert([ - 'OrderID' => $internalOID, - 'TestSiteID' => $testSiteID, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - // Execute multiple actions - $actions = [ - [ - 'type' => 'RESULT_SET', - 'value' => 7.2, - ], - [ - 'type' => 'COMMENT_INSERT', - 'comment' => 'Multi-action test', - ], - [ - 'type' => 'TEST_INSERT', - 'testCode' => 'HBA1C', - ], - ]; - - $context = [ - 'order' => [ - 'InternalOID' => $internalOID, - ], - 'testSiteID' => $testSiteID, - ]; - - foreach ($actions as $action) { - $this->invokeMethod($this->engine, 'executeAction', [$action, $context]); - } - - // Verify SET_RESULT - $patres = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->where('DelDate', null) - ->get()->getRowArray(); - $this->assertEquals(7.2, $patres['Result']); - - // Verify ADD_COMMENT - $comment = $db->table('ordercom') - ->where('InternalOID', $internalOID) - ->where('Comment', 'Multi-action test') - ->get()->getRowArray(); - $this->assertNotNull($comment); - - // Verify INSERT_TEST - $hba1cSite = $db->table('testdefsite')->where('TestSiteCode', 'HBA1C')->get()->getRowArray(); - $hba1cPatres = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $hba1cSite['TestSiteID']) - ->where('DelDate', null) - ->get()->getRowArray(); - $this->assertNotNull($hba1cPatres); - } - - public function testIsTestRequested() - { - $db = \Config\Database::connect(); - - // Get order ID - $order = $db->table('orders')->where('OrderID', 'ORD001')->get()->getRowArray(); - $internalOID = $order['InternalOID'] ?? null; - - // Get test site ID - $testSite = $db->table('testdefsite')->where('TestSiteCode', 'GLU')->get()->getRowArray(); - $testSiteID = $testSite['TestSiteID'] ?? null; - - // Insert patres row for GLU test - $db->table('patres')->insert([ - 'OrderID' => $internalOID, - 'TestSiteID' => $testSiteID, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - // Test isTestRequested method - $result = $this->invokeMethod($this->engine, 'isTestRequested', ['GLU', [ - 'order' => ['InternalOID' => $internalOID], - ]]); - - $this->assertTrue($result); - - // Test for non-existent test - $result = $this->invokeMethod($this->engine, 'isTestRequested', ['NONEXISTENT', [ - 'order' => ['InternalOID' => $internalOID], - ]]); - - $this->assertFalse($result); - } - - public function testExecuteDeleteTest(): void - { - $db = \Config\Database::connect(); - - $order = $db->table('orders')->where('OrderID', 'ORD001')->get()->getRowArray(); - $internalOID = $order['InternalOID'] ?? null; - $this->assertNotNull($internalOID); - - $testSite = $db->table('testdefsite')->where('TestSiteCode', 'HBA1C')->get()->getRowArray(); - $testSiteID = $testSite['TestSiteID'] ?? null; - $this->assertNotNull($testSiteID); - - // Insert a patres row to delete - $db->table('patres')->insert([ - 'OrderID' => $internalOID, - 'TestSiteID' => $testSiteID, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - - $action = [ - 'type' => 'TEST_DELETE', - 'testCode' => 'HBA1C', - ]; - - $context = [ - 'order' => [ - 'InternalOID' => $internalOID, - ], - ]; - - $this->invokeMethod($this->engine, 'executeAction', [$action, $context]); - - $deleted = $db->table('patres') - ->where('OrderID', $internalOID) - ->where('TestSiteID', $testSiteID) - ->get() - ->getRowArray(); - - $this->assertNotNull($deleted); - $this->assertNotEmpty($deleted['DelDate'] ?? null); - } - - /** - * Helper to invoke protected/private methods - */ - private function invokeMethod($object, $methodName, array $parameters = []) - { - $reflection = new \ReflectionClass(get_class($object)); - $method = $reflection->getMethod($methodName); - $method->setAccessible(true); - return $method->invokeArgs($object, $parameters); - } -} diff --git a/tests/unit/Rules/RuleExpressionCompileTest.php b/tests/unit/Rules/RuleExpressionCompileTest.php deleted file mode 100644 index 5b5f80e..0000000 --- a/tests/unit/Rules/RuleExpressionCompileTest.php +++ /dev/null @@ -1,60 +0,0 @@ -markTestSkipped('Symfony ExpressionLanguage not installed'); - } - } - - public function testCompileSexCondition(): void - { - $svc = new RuleExpressionService(); - - $compiled = $svc->compile("if(sex('F') ? result_set(0.7) : result_set(1))"); - - $this->assertIsArray($compiled); - $this->assertEquals('patient["Sex"] == "F"', $compiled['conditionExpr']); - $this->assertEquals(0.7, $compiled['then'][0]['value']); - $this->assertEquals(1, $compiled['else'][0]['value']); - $this->assertStringContainsString('patient["Sex"] == "F"', $compiled['valueExpr']); - } - - public function testCompilePriorityCondition(): void - { - $svc = new RuleExpressionService(); - - $compiled = $svc->compile("if(priority('S') ? result_set('urgent') : result_set('normal'))"); - - $this->assertIsArray($compiled); - $this->assertEquals('order["Priority"] == "S"', $compiled['conditionExpr']); - $this->assertEquals('urgent', $compiled['then'][0]['value']); - $this->assertEquals('normal', $compiled['else'][0]['value']); - } - - public function testCompileInvalidSyntax(): void - { - $svc = new RuleExpressionService(); - - $this->expectException(\InvalidArgumentException::class); - $svc->compile("invalid syntax here"); - } - - public function testCompileEmptyReturnsEmpty(): void - { - $svc = new RuleExpressionService(); - - $compiled = $svc->compile(""); - - $this->assertIsArray($compiled); - $this->assertEmpty($compiled); - } -} diff --git a/tests/unit/Rules/RuleExpressionServiceTest.php b/tests/unit/Rules/RuleExpressionServiceTest.php deleted file mode 100644 index b6c2a3c..0000000 --- a/tests/unit/Rules/RuleExpressionServiceTest.php +++ /dev/null @@ -1,24 +0,0 @@ -evaluateBoolean('order["SiteID"] == 1', [ - 'order' => ['SiteID' => 1], - ]); - $this->assertTrue($ok); - - $no = $svc->evaluateBoolean('order["SiteID"] == 2', [ - 'order' => ['SiteID' => 1], - ]); - $this->assertFalse($no); - } -} diff --git a/tests/unit/Rules/RuleExpressionSyntaxTest.php b/tests/unit/Rules/RuleExpressionSyntaxTest.php deleted file mode 100644 index c5ce62c..0000000 --- a/tests/unit/Rules/RuleExpressionSyntaxTest.php +++ /dev/null @@ -1,346 +0,0 @@ -service = new RuleExpressionService(); - } - - // ============================================ - // SYNTAX TESTS - // ============================================ - - public function testSemicolonSyntaxBasic() - { - $result = $this->service->compile('if(sex("M"); result_set(0.5); result_set(0.6))'); - - $this->assertArrayHasKey('conditionExpr', $result); - $this->assertArrayHasKey('valueExpr', $result); - $this->assertArrayHasKey('then', $result); - $this->assertArrayHasKey('else', $result); - - $this->assertEquals('patient["Sex"] == "M"', $result['conditionExpr']); - $this->assertEquals('0.5', $result['then'][0]['valueExpr']); - $this->assertEquals('0.6', $result['else'][0]['valueExpr']); - } - - public function testTernarySyntaxFallback() - { - // Legacy ternary syntax should still work (fallback) - $result = $this->service->compile('if(sex("M") ? result_set(0.5) : result_set(0.6))'); - - $this->assertArrayHasKey('conditionExpr', $result); - $this->assertEquals('patient["Sex"] == "M"', $result['conditionExpr']); - } - - // ============================================ - // MULTI-ACTION TESTS - // ============================================ - - public function testMultiActionWithColon() - { - $result = $this->service->compile('if(sex("M"); result_set(0.5):test_insert("HBA1C"); nothing)'); - - $this->assertCount(2, $result['then']); - $this->assertEquals('RESULT_SET', $result['then'][0]['type']); - $this->assertEquals(0.5, $result['then'][0]['value']); - $this->assertEquals('TEST_INSERT', $result['then'][1]['type']); - $this->assertEquals('HBA1C', $result['then'][1]['testCode']); - $this->assertEquals('NO_OP', $result['else'][0]['type']); - } - - public function testThreeActions() - { - $result = $this->service->compile('if(priority("S"); result_set("URGENT"):test_insert("STAT_TEST"):comment_insert("Stat order"); nothing)'); - - $this->assertCount(3, $result['then']); - $this->assertEquals('RESULT_SET', $result['then'][0]['type']); - $this->assertEquals('URGENT', $result['then'][0]['value']); - $this->assertEquals('TEST_INSERT', $result['then'][1]['type']); - $this->assertEquals('STAT_TEST', $result['then'][1]['testCode']); - $this->assertEquals('COMMENT_INSERT', $result['then'][2]['type']); - $this->assertEquals('Stat order', $result['then'][2]['comment']); - } - - public function testMultiActionInElse() - { - $result = $this->service->compile('if(sex("M"); result_set(1.0); result_set(0.8):comment_insert("Default"))'); - - $this->assertCount(1, $result['then']); - $this->assertCount(2, $result['else']); - $this->assertEquals('RESULT_SET', $result['else'][0]['type']); - $this->assertEquals(0.8, $result['else'][0]['value']); - $this->assertEquals('COMMENT_INSERT', $result['else'][1]['type']); - } - - // ============================================ - // LOGICAL OPERATOR TESTS - // ============================================ - - public function testAndOperator() - { - $result = $this->service->compile('if(sex("M") && age > 40; result_set(1.2); result_set(1.0))'); - - $this->assertStringContainsString('and', strtolower($result['conditionExpr'])); - $this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']); - $this->assertStringContainsString('age > 40', $result['conditionExpr']); - } - - public function testOrOperator() - { - $result = $this->service->compile('if(sex("M") || age > 65; result_set(1.0); result_set(0.8))'); - - $this->assertStringContainsString('or', strtolower($result['conditionExpr'])); - $this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']); - $this->assertStringContainsString('age > 65', $result['conditionExpr']); - } - - public function testCombinedAndOr() - { - $result = $this->service->compile('if((sex("M") && age > 40) || (sex("F") && age > 50); result_set(1.5); result_set(1.0))'); - - $this->assertStringContainsString('or', strtolower($result['conditionExpr'])); - $this->assertStringContainsString('and', strtolower($result['conditionExpr'])); - } - - public function testComplexNestedCondition() - { - $result = $this->service->compile('if(sex("M") && (age > 40 || priority("S")); result_set(1.2); nothing)'); - - $this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']); - $this->assertStringContainsString('or', strtolower($result['conditionExpr'])); - } - - // ============================================ - // ACTION TESTS - // ============================================ - - public function testNothingAction() - { - $result = $this->service->compile('if(sex("M"); result_set(0.5); nothing)'); - - $this->assertEquals('NO_OP', $result['else'][0]['type']); - } - - public function testNothingActionInThen() - { - $result = $this->service->compile('if(sex("M"); nothing; result_set(0.6))'); - - $this->assertEquals('NO_OP', $result['then'][0]['type']); - } - - public function testInsertAction() - { - $result = $this->service->compile('if(requested("GLU"); test_insert("HBA1C"); nothing)'); - - $this->assertEquals('TEST_INSERT', $result['then'][0]['type']); - $this->assertEquals('HBA1C', $result['then'][0]['testCode']); - } - - public function testAddCommentAction() - { - $result = $this->service->compile('if(sex("M"); comment_insert("Male patient"); nothing)'); - - $this->assertEquals('COMMENT_INSERT', $result['then'][0]['type']); - $this->assertEquals('Male patient', $result['then'][0]['comment']); - } - - // ============================================ - // CONDITION TESTS - // ============================================ - - public function testSexCondition() - { - $result = $this->service->compile('if(sex("F"); result_set(0.7); result_set(1.0))'); - - $this->assertEquals('patient["Sex"] == "F"', $result['conditionExpr']); - } - - public function testPriorityCondition() - { - $result = $this->service->compile('if(priority("S"); result_set("URGENT"); result_set("NORMAL"))'); - - $this->assertEquals('order["Priority"] == "S"', $result['conditionExpr']); - } - - public function testPriorityUrgent() - { - $result = $this->service->compile('if(priority("U"); result_set("CRITICAL"); nothing)'); - - $this->assertEquals('order["Priority"] == "U"', $result['conditionExpr']); - } - - public function testAgeCondition() - { - $result = $this->service->compile('if(age > 18; result_set(1.0); result_set(0.5))'); - - $this->assertEquals('age > 18', $result['conditionExpr']); - } - - public function testAgeGreaterThanEqual() - { - $result = $this->service->compile('if(age >= 18; result_set(1.0); nothing)'); - - $this->assertEquals('age >= 18', $result['conditionExpr']); - } - - public function testAgeLessThan() - { - $result = $this->service->compile('if(age < 65; result_set(1.0); nothing)'); - - $this->assertEquals('age < 65', $result['conditionExpr']); - } - - public function testAgeRange() - { - $result = $this->service->compile('if(age >= 18 && age <= 65; result_set(1.0); nothing)'); - - $this->assertStringContainsString('age >= 18', $result['conditionExpr']); - $this->assertStringContainsString('age <= 65', $result['conditionExpr']); - } - - public function testRequestedCondition() - { - $result = $this->service->compile('if(requested("GLU"); test_insert("HBA1C"); nothing)'); - - $this->assertStringContainsString('requested', $result['conditionExpr']); - $this->assertStringContainsString('GLU', $result['conditionExpr']); - } - - // ============================================ - // MULTI-RULE TESTS - // ============================================ - - public function testParseMultiRule() - { - $expr = 'if(sex("M"); result_set(0.5); nothing), if(age > 65; result_set(1.0); nothing)'; - $rules = $this->service->parseMultiRule($expr); - - $this->assertCount(2, $rules); - $this->assertStringContainsString('sex("M")', $rules[0]); - $this->assertStringContainsString('age > 65', $rules[1]); - } - - public function testParseMultiRuleThreeRules() - { - $expr = 'if(sex("M"); result_set(0.5); nothing), if(age > 65; result_set(1.0); nothing), if(priority("S"); result_set(2.0); nothing)'; - $rules = $this->service->parseMultiRule($expr); - - $this->assertCount(3, $rules); - } - - // ============================================ - // ERROR HANDLING TESTS - // ============================================ - - public function testInvalidSyntaxThrowsException() - { - $this->expectException(\InvalidArgumentException::class); - $this->service->compile('invalid syntax here'); - } - - public function testUnknownActionThrowsException() - { - $this->expectException(\InvalidArgumentException::class); - $this->service->compile('if(sex("M"); unknown_action(); nothing)'); - } - - public function testEmptyExpressionReturnsEmptyArray() - { - $result = $this->service->compile(''); - - $this->assertEmpty($result); - } - - // ============================================ - // DOCUMENTATION EXAMPLES - // ============================================ - - public function testExample1SexBasedResult() - { - // Example 1 from docs - $result = $this->service->compile('if(sex("M"); result_set(0.5); result_set(0.6))'); - - $this->assertEquals('RESULT_SET', $result['then'][0]['type']); - $this->assertEquals(0.5, $result['then'][0]['value']); - $this->assertEquals('RESULT_SET', $result['else'][0]['type']); - $this->assertEquals(0.6, $result['else'][0]['value']); - } - - public function testExample2ConditionalTestInsertion() - { - // Example 2 from docs - $result = $this->service->compile("if(requested('GLU'); test_insert('HBA1C'):test_insert('INS'); nothing)"); - - $this->assertCount(2, $result['then']); - $this->assertEquals('TEST_INSERT', $result['then'][0]['type']); - $this->assertEquals('HBA1C', $result['then'][0]['testCode']); - $this->assertEquals('INS', $result['then'][1]['testCode']); - } - - public function testExample3MultipleConditionsAnd() - { - // Example 3 from docs - $result = $this->service->compile("if(sex('M') && age > 40; result_set(1.2); result_set(1.0))"); - - $this->assertStringContainsString('and', strtolower($result['conditionExpr'])); - } - - public function testExample4OrCondition() - { - // Example 4 from docs - $result = $this->service->compile("if(sex('M') || age > 65; result_set(1.0); result_set(0.8))"); - - $this->assertStringContainsString('or', strtolower($result['conditionExpr'])); - } - - public function testExample5CombinedAndOr() - { - // Example 5 from docs - $result = $this->service->compile("if((sex('M') && age > 40) || (sex('F') && age > 50); result_set(1.5); result_set(1.0))"); - - $this->assertStringContainsString('or', strtolower($result['conditionExpr'])); - } - - public function testExample6MultipleActions() - { - // Example 6 from docs - $result = $this->service->compile("if(priority('S'); result_set('URGENT'):test_insert('STAT_TEST'); result_set('NORMAL'))"); - - $this->assertCount(2, $result['then']); - $this->assertEquals('RESULT_SET', $result['then'][0]['type']); - $this->assertEquals('TEST_INSERT', $result['then'][1]['type']); - } - - public function testExample7ThreeActions() - { - // Example 7 from docs - $result = $this->service->compile("if(sex('M') && age > 40; result_set(1.5):test_insert('EXTRA_TEST'):comment_insert('Male over 40'); nothing)"); - - $this->assertCount(3, $result['then']); - $this->assertEquals('RESULT_SET', $result['then'][0]['type']); - $this->assertEquals('TEST_INSERT', $result['then'][1]['type']); - $this->assertEquals('COMMENT_INSERT', $result['then'][2]['type']); - } - - public function testExample8ComplexRule() - { - // Example 8 from docs - $result = $this->service->compile("if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'):comment_insert('Female stat 18-50'); result_set('NORMAL'))"); - - $this->assertCount(2, $result['then']); - $this->assertStringContainsString('and', strtolower($result['conditionExpr'])); - } -} diff --git a/tests/unit/TestDef/TestDefModelsTest.php b/tests/unit/TestDef/TestDefModelsTest.php deleted file mode 100644 index 476815e..0000000 --- a/tests/unit/TestDef/TestDefModelsTest.php +++ /dev/null @@ -1,191 +0,0 @@ -testDefSiteModel = new TestDefSiteModel(); - - $this->testDefCalModel = new TestDefCalModel(); - $this->testDefGrpModel = new TestDefGrpModel(); - $this->testMapModel = new TestMapModel(); - } - - /** - * Test TestDefSiteModel has correct table name - */ - public function testTestDefSiteModelTable() - { - $this->assertEquals('testdefsite', $this->testDefSiteModel->table); - } - - /** - * Test TestDefSiteModel has correct primary key - */ - public function testTestDefSiteModelPrimaryKey() - { - $this->assertEquals('TestSiteID', $this->testDefSiteModel->primaryKey); - } - - /** - * Test TestDefSiteModel has correct allowed fields - */ - public function testTestDefSiteModelAllowedFields() - { - $allowedFields = $this->testDefSiteModel->allowedFields; - - $this->assertContains('SiteID', $allowedFields); - $this->assertContains('TestSiteCode', $allowedFields); - $this->assertContains('TestSiteName', $allowedFields); - $this->assertContains('TestType', $allowedFields); - $this->assertContains('Description', $allowedFields); - $this->assertContains('SeqScr', $allowedFields); - $this->assertContains('SeqRpt', $allowedFields); - $this->assertContains('IndentLeft', $allowedFields); - $this->assertContains('FontStyle', $allowedFields); - $this->assertContains('VisibleScr', $allowedFields); - $this->assertContains('VisibleRpt', $allowedFields); - $this->assertContains('CountStat', $allowedFields); - $this->assertContains('CreateDate', $allowedFields); - $this->assertContains('StartDate', $allowedFields); - $this->assertContains('EndDate', $allowedFields); - $this->assertContains('ResultType', $allowedFields); - $this->assertContains('RefType', $allowedFields); - } - - /** - * Test TestDefSiteModel uses soft deletes - */ - public function testTestDefSiteModelSoftDeletes() - { - $this->assertTrue($this->testDefSiteModel->useSoftDeletes); - $this->assertEquals('EndDate', $this->testDefSiteModel->deletedField); - } - - /** - * Test TestDefCalModel has correct table name - */ - public function testTestDefCalModelTable() - { - $this->assertEquals('testdefcal', $this->testDefCalModel->table); - } - - /** - * Test TestDefCalModel has correct primary key - */ - public function testTestDefCalModelPrimaryKey() - { - $this->assertEquals('TestCalID', $this->testDefCalModel->primaryKey); - } - - /** - * Test TestDefCalModel has correct allowed fields - */ - public function testTestDefCalModelAllowedFields() - { - $allowedFields = $this->testDefCalModel->allowedFields; - - $this->assertContains('TestSiteID', $allowedFields); - $this->assertContains('DisciplineID', $allowedFields); - $this->assertContains('DepartmentID', $allowedFields); - $this->assertContains('FormulaCode', $allowedFields); - $this->assertContains('RefType', $allowedFields); - $this->assertContains('Unit1', $allowedFields); - $this->assertContains('Factor', $allowedFields); - $this->assertContains('Unit2', $allowedFields); - $this->assertContains('Decimal', $allowedFields); - $this->assertContains('Method', $allowedFields); - } - - /** - * Test TestDefGrpModel has correct table name - */ - public function testTestDefGrpModelTable() - { - $this->assertEquals('testdefgrp', $this->testDefGrpModel->table); - } - - /** - * Test TestDefGrpModel has correct primary key - */ - public function testTestDefGrpModelPrimaryKey() - { - $this->assertEquals('TestGrpID', $this->testDefGrpModel->primaryKey); - } - - /** - * Test TestDefGrpModel has correct allowed fields - */ - public function testTestDefGrpModelAllowedFields() - { - $allowedFields = $this->testDefGrpModel->allowedFields; - - $this->assertContains('TestSiteID', $allowedFields); - $this->assertContains('Member', $allowedFields); - } - - /** - * Test TestMapModel has correct table name - */ - public function testTestMapModelTable() - { - $this->assertEquals('testmap', $this->testMapModel->table); - } - - /** - * Test TestMapModel has correct primary key - */ - public function testTestMapModelPrimaryKey() - { - $this->assertEquals('TestMapID', $this->testMapModel->primaryKey); - } - - /** - * Test TestMapModel has correct allowed fields - */ - public function testTestMapModelAllowedFields() - { - $allowedFields = $this->testMapModel->allowedFields; - - $this->assertContains('HostType', $allowedFields); - $this->assertContains('HostID', $allowedFields); - $this->assertContains('ClientType', $allowedFields); - $this->assertContains('ClientID', $allowedFields); - } - - /** - * Test all models use soft deletes - */ - public function testAllModelsUseSoftDeletes() - { - $this->assertTrue($this->testDefCalModel->useSoftDeletes); - $this->assertTrue($this->testDefGrpModel->useSoftDeletes); - $this->assertTrue($this->testMapModel->useSoftDeletes); - } - - /** - * Test all models have EndDate as deleted field - */ - public function testAllModelsUseEndDateAsDeletedField() - { - $this->assertEquals('EndDate', $this->testDefCalModel->deletedField); - $this->assertEquals('EndDate', $this->testDefGrpModel->deletedField); - $this->assertEquals('EndDate', $this->testMapModel->deletedField); - } -} diff --git a/tests/unit/ValueSet/ValueSetTest.php b/tests/unit/ValueSet/ValueSetTest.php index a3e8135..e927d1c 100644 --- a/tests/unit/ValueSet/ValueSetTest.php +++ b/tests/unit/ValueSet/ValueSetTest.php @@ -11,7 +11,7 @@ class ValueSetTest extends CIUnitTestCase { parent::setUp(); ValueSet::clearCache(); - } + } public function testGetPatientSexReturnsFormattedArray() { @@ -28,9 +28,9 @@ class ValueSetTest extends CIUnitTestCase $values = array_column($result, 'value'); $labels = array_column($result, 'label'); - $this->assertContains('1', $values); - $this->assertContains('2', $values); - $this->assertContains('3', $values); + $this->assertContains('F', $values); + $this->assertContains('M', $values); + $this->assertContains('U', $values); $this->assertContains('Female', $labels); $this->assertContains('Male', $labels); $this->assertContains('Unknown', $labels); @@ -51,17 +51,17 @@ class ValueSetTest extends CIUnitTestCase $keys = array_column($result, 'key'); $values = array_column($result, 'value'); - $this->assertContains('1', $keys); - $this->assertContains('2', $keys); + $this->assertContains('F', $keys); + $this->assertContains('M', $keys); $this->assertContains('Female', $values); $this->assertContains('Male', $values); } public function testGetLabelConvertsCodeToLabel() { - $this->assertEquals('Female', ValueSet::getLabel('sex', '1')); - $this->assertEquals('Male', ValueSet::getLabel('sex', '2')); - $this->assertEquals('Unknown', ValueSet::getLabel('sex', '3')); + $this->assertEquals('Female', ValueSet::getLabel('sex', 'F')); + $this->assertEquals('Male', ValueSet::getLabel('sex', 'M')); + $this->assertEquals('Unknown', ValueSet::getLabel('sex', 'U')); } public function testGetLabelForOrderPriority() @@ -118,10 +118,10 @@ class ValueSetTest extends CIUnitTestCase public function testGetPatientSex() { $result = ValueSet::get('sex'); - $this->assertEquals('1', $result[0]['value']); - $this->assertEquals('Female', $result[0]['label']); - $this->assertEquals('2', $result[1]['value']); - $this->assertEquals('Male', $result[1]['label']); + $this->assertEquals('F', $result[0]['value']); + $this->assertEquals('Female', $result[0]['label']); + $this->assertEquals('M', $result[1]['value']); + $this->assertEquals('Male', $result[1]['label']); } public function testGetSpecimenStatus() @@ -270,10 +270,10 @@ class ValueSetTest extends CIUnitTestCase public function testGetReturnsFormattedValues() { $result = ValueSet::get('sex'); - $this->assertEquals('1', $result[0]['value']); - $this->assertEquals('Female', $result[0]['label']); - $this->assertEquals('2', $result[1]['value']); - $this->assertEquals('Male', $result[1]['label']); + $this->assertEquals('F', $result[0]['value']); + $this->assertEquals('Female', $result[0]['label']); + $this->assertEquals('M', $result[1]['value']); + $this->assertEquals('Male', $result[1]['label']); } public function testGetWithSpecialCharactersInKey() @@ -354,20 +354,20 @@ class ValueSetTest extends CIUnitTestCase public function testTransformLabels() { - $data = [ - ['Gender' => '1', 'Country' => 'IDN'], - ['Gender' => '2', 'Country' => 'USA'] - ]; + $data = [ + ['Gender' => 'F', 'Country' => 'IDN'], + ['Gender' => 'M', 'Country' => 'USA'] + ]; $result = ValueSet::transformLabels($data, [ 'Gender' => 'sex', 'Country' => 'country' ]); - $this->assertEquals('1', $result[0]['Gender']); - $this->assertEquals('Female', $result[0]['GenderLabel']); - $this->assertEquals('2', $result[1]['Gender']); - $this->assertEquals('Male', $result[1]['GenderLabel']); + $this->assertEquals('F', $result[0]['Gender']); + $this->assertEquals('Female', $result[0]['GenderLabel']); + $this->assertEquals('M', $result[1]['Gender']); + $this->assertEquals('Male', $result[1]['GenderLabel']); $this->assertEquals('USA', $result[1]['Country']); $this->assertEquals('United States of America', $result[1]['CountryLabel']); }