feat: require id params for update endpoints
This commit is contained in:
parent
2bcdf09b55
commit
aaadd593dd
468
AGENTS.md
468
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 <name>
|
||||
php spark make:model <name>
|
||||
php spark make:controller <name>
|
||||
|
||||
# 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
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use App\Traits\ResponseTrait;
|
||||
use Firebase\JWT\JWT;
|
||||
```
|
||||
|
||||
### Controller Structure
|
||||
Controllers handle HTTP requests and delegate business logic to Models. They should NOT contain database queries.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Traits\ResponseTrait;
|
||||
|
||||
class ExampleController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class PatientModel extends BaseModel
|
||||
{
|
||||
protected $table = 'patients';
|
||||
protected $primaryKey = 'PatientID';
|
||||
protected $allowedFields = ['NameFirst', 'NameLast', ...];
|
||||
|
||||
private function checkDbError($db, string $context) {
|
||||
$error = $db->error();
|
||||
if (!empty($error['code'])) {
|
||||
throw new \Exception("{$context} failed: {$error['code']} - {$error['message']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Guidelines
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Patients;
|
||||
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use Faker\Factory;
|
||||
|
||||
class PatientCreateTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected $endpoint = 'api/patient';
|
||||
|
||||
public function testCreatePatientSuccess()
|
||||
{
|
||||
$faker = Factory::create('id_ID');
|
||||
$payload = [...];
|
||||
$result = $this->withBodyFormat('json')->post($this->endpoint, $payload);
|
||||
$result->assertStatus(201);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test Naming**: `test<Action><Scenario><ExpectedResult>` (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/<resource>.yaml` (e.g., `patients.yaml`, `orders.yaml`)
|
||||
- **Schemas**: `public/components/schemas/<resource>.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 <Name>
|
||||
php spark make:controller <Name>
|
||||
php spark make:migration <name>
|
||||
|
||||
# Database migrations
|
||||
php spark migrate
|
||||
php spark migrate:rollback
|
||||
|
||||
# After OpenAPI edits
|
||||
node public/bundle-api-docs.js
|
||||
```
|
||||
|
||||
Use `php spark test --filter <Class>::<method>` 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<Action><Scenario><Result>` (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/<resource>.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/<resource>')` 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.
|
||||
|
||||
14
README.md
14
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:**
|
||||
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
/*
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||
bootstrap="vendor/codeigniter4/framework/system/Test/bootstrap.php"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||
bootstrap="tests/phpunit-bootstrap.php"
|
||||
backupGlobals="false"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
colors="true"
|
||||
@ -42,8 +42,8 @@
|
||||
</exclude>
|
||||
</source>
|
||||
<php>
|
||||
<!-- Enable / Disable --> <!-- WAJIB DISESUAIKAN -->
|
||||
<!-- <env name="CI_ENVIRONMENT" value="testing"/> -->
|
||||
<!-- Enable / Disable --> <!-- WAJIB DISESUAIKAN -->
|
||||
<env name="CI_ENVIRONMENT" value="testing"/>
|
||||
|
||||
<!-- <server name="app.baseURL" value="http://example.com/"/> -->
|
||||
<server name="app.baseURL" value="http://localhost/clqms01/"/> <!-- WAJIB DISESUAIKAN -->
|
||||
@ -56,7 +56,7 @@
|
||||
<const name="PUBLICPATH" value="./public/"/>
|
||||
<!-- Database configuration -->
|
||||
<env name="database.tests.hostname" value="localhost"/> <!-- WAJIB DISESUAIKAN -->
|
||||
<env name="database.tests.database" value="clqms01"/> <!-- WAJIB DISESUAIKAN -->
|
||||
<env name="database.tests.database" value="clqms01_test"/> <!-- WAJIB DISESUAIKAN -->
|
||||
<env name="database.tests.username" value="root"/> <!-- WAJIB DISESUAIKAN -->
|
||||
<env name="database.tests.password" value="adminsakti"/> <!-- WAJIB DISESUAIKAN -->
|
||||
<env name="database.tests.DBDriver" value="MySQLi"/> <!-- WAJIB DISESUAIKAN -->
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||
|
||||
@ -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'
|
||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: []
|
||||
|
||||
@ -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: []
|
||||
|
||||
@ -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
|
||||
description: Server error
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Support\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class ExampleMigration extends Migration
|
||||
{
|
||||
protected $DBGroup = 'tests';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
62
tests/_support/Traits/CreatesPatients.php
Normal file
62
tests/_support/Traits/CreatesPatients.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Support\Traits;
|
||||
|
||||
use App\Models\Patient\PatientModel;
|
||||
use Faker\Factory;
|
||||
|
||||
trait CreatesPatients
|
||||
{
|
||||
protected function createTestPatient(array $overrides = []): int
|
||||
{
|
||||
$faker = Factory::create('id_ID');
|
||||
$patientPayload = array_merge([
|
||||
'PatientID' => '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;
|
||||
}
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Calculator;
|
||||
|
||||
use App\Models\Test\TestDefCalModel;
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
|
||||
class CalculatorEndpointTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected TestDefSiteModel $siteModel;
|
||||
protected TestDefCalModel $calcModel;
|
||||
protected ?int $siteId = null;
|
||||
protected ?int $calcId = null;
|
||||
protected string $calcName;
|
||||
protected string $calcCode;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@ -1,250 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Calculator;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Services\CalculatorService;
|
||||
|
||||
class CalculatorTest extends CIUnitTestCase
|
||||
{
|
||||
protected CalculatorService $calculator;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -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 = [])
|
||||
{
|
||||
|
||||
@ -1,285 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Orders;
|
||||
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use Faker\Factory;
|
||||
|
||||
class OrderCreateTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected $endpoint = 'api/ordertest';
|
||||
|
||||
public function testCreateOrderSuccess()
|
||||
{
|
||||
$internalPID = $this->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;
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
];
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = [])
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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']));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class SimpleTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
public function testTrue()
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
443
tests/feature/Test/TestCreateVariantsTest.php
Normal file
443
tests/feature/Test/TestCreateVariantsTest.php
Normal file
@ -0,0 +1,443 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Test;
|
||||
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
|
||||
class TestCreateVariantsTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
private const SITE_ID = 1;
|
||||
|
||||
protected string $endpoint = 'api/test';
|
||||
|
||||
private TestDefSiteModel $testModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@ -1,417 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
class TestsControllerTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Generate JWT Token
|
||||
$key = getenv('JWT_SECRET') ?: 'my-secret-key';
|
||||
$payload = [
|
||||
'iss' => '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']);
|
||||
}
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class UniformShowTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected $endpoint = 'api';
|
||||
protected $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Generate Token
|
||||
$key = getenv('JWT_SECRET') ?: 'my-secret-key'; // Fallback if env not loaded in test
|
||||
$payload = [
|
||||
'iss' => '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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
46
tests/phpunit-bootstrap.php
Normal file
46
tests/phpunit-bootstrap.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/codeigniter4/framework/system/Test/bootstrap.php';
|
||||
|
||||
use CodeIgniter\Database\MigrationRunner;
|
||||
use Config\Database;
|
||||
use Config\Migrations as MigrationsConfig;
|
||||
|
||||
if (defined('CLQMS_PHPUNIT_BOOTSTRAPPED') || ENVIRONMENT !== 'testing') {
|
||||
return;
|
||||
}
|
||||
|
||||
define('CLQMS_PHPUNIT_BOOTSTRAPPED', true);
|
||||
|
||||
$db = Database::connect('tests');
|
||||
$forge = Database::forge('tests');
|
||||
|
||||
$db->query('SET FOREIGN_KEY_CHECKS=0');
|
||||
foreach ($db->listTables() as $table) {
|
||||
$forge->dropTable($table, true);
|
||||
}
|
||||
$db->query('SET FOREIGN_KEY_CHECKS=1');
|
||||
|
||||
$migrationsConfig = config(MigrationsConfig::class);
|
||||
$migrationRunner = new MigrationRunner($migrationsConfig, 'tests');
|
||||
try {
|
||||
$migrationRunner->latest();
|
||||
} catch (DatabaseException $e) {
|
||||
$message = $e->getMessage();
|
||||
if (strpos($message, 'already exists') === false) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$initialBufferLevel = ob_get_level();
|
||||
ob_start();
|
||||
try {
|
||||
$seeder = Database::seeder('tests');
|
||||
$seeder->setSilent(true)->call('DBSeeder');
|
||||
} finally {
|
||||
while (ob_get_level() > $initialBufferLevel) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
@ -1,271 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Rule;
|
||||
|
||||
use App\Models\Rule\RuleDefModel;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\DatabaseTestTrait;
|
||||
|
||||
class RuleDefModelTest extends CIUnitTestCase
|
||||
{
|
||||
use DatabaseTestTrait;
|
||||
|
||||
protected $model;
|
||||
protected $seed = \App\Database\Seeds\TestSeeder::class;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -1,355 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Rules;
|
||||
|
||||
use App\Services\RuleEngineService;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\DatabaseTestTrait;
|
||||
|
||||
/**
|
||||
* Integration tests for Rule Engine with multi-action support
|
||||
*/
|
||||
class RuleEngineMultiActionTest extends CIUnitTestCase
|
||||
{
|
||||
use DatabaseTestTrait;
|
||||
|
||||
protected RuleEngineService $engine;
|
||||
protected $seed = [];
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Rules;
|
||||
|
||||
use App\Services\RuleExpressionService;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class RuleExpressionCompileTest extends CIUnitTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Rules;
|
||||
|
||||
use App\Services\RuleExpressionService;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class RuleExpressionServiceTest extends CIUnitTestCase
|
||||
{
|
||||
public function testEvaluateBooleanWithArrayContext(): void
|
||||
{
|
||||
$svc = new RuleExpressionService();
|
||||
|
||||
$ok = $svc->evaluateBoolean('order["SiteID"] == 1', [
|
||||
'order' => ['SiteID' => 1],
|
||||
]);
|
||||
$this->assertTrue($ok);
|
||||
|
||||
$no = $svc->evaluateBoolean('order["SiteID"] == 2', [
|
||||
'order' => ['SiteID' => 1],
|
||||
]);
|
||||
$this->assertFalse($no);
|
||||
}
|
||||
}
|
||||
@ -1,346 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Rules;
|
||||
|
||||
use App\Services\RuleExpressionService;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
/**
|
||||
* Tests for Rule DSL syntax - semicolon syntax, multi-actions, operators
|
||||
*/
|
||||
class RuleExpressionSyntaxTest extends CIUnitTestCase
|
||||
{
|
||||
protected RuleExpressionService $service;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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']));
|
||||
}
|
||||
}
|
||||
@ -1,191 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
|
||||
use App\Models\Test\TestDefCalModel;
|
||||
use App\Models\Test\TestDefGrpModel;
|
||||
use App\Models\Test\TestMapModel;
|
||||
|
||||
class TestDefModelsTest extends CIUnitTestCase
|
||||
{
|
||||
protected $testDefSiteModel;
|
||||
|
||||
protected $testDefCalModel;
|
||||
protected $testDefGrpModel;
|
||||
protected $testMapModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -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']);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user