feat: require id params for update endpoints
This commit is contained in:
parent
2bcdf09b55
commit
aaadd593dd
406
AGENTS.md
406
AGENTS.md
@ -1,316 +1,152 @@
|
|||||||
# AGENTS.md - Code Guidelines for CLQMS
|
# 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.
|
> **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Build, Test & Lint Commands
|
## 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
|
```bash
|
||||||
# Run all tests
|
# Run the entire PHPUnit suite
|
||||||
./vendor/bin/phpunit
|
./vendor/bin/phpunit
|
||||||
|
|
||||||
# Run a specific test file
|
# Target a single test file (fast verification)
|
||||||
./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php
|
./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php
|
||||||
|
|
||||||
# Run a specific test method
|
# Run one test case by method
|
||||||
./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php
|
./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php
|
||||||
|
|
||||||
# Run tests with coverage
|
# Generate scaffolding (model, controller, migration)
|
||||||
./vendor/bin/phpunit --coverage-html build/logs/html
|
php spark make:model <Name>
|
||||||
|
php spark make:controller <Name>
|
||||||
# Run tests by suite
|
|
||||||
./vendor/bin/phpunit --testsuite App
|
|
||||||
|
|
||||||
# Generate scaffolding
|
|
||||||
php spark make:migration <name>
|
php spark make:migration <name>
|
||||||
php spark make:model <name>
|
|
||||||
php spark make:controller <name>
|
|
||||||
|
|
||||||
# Database migrations
|
# Database migrations
|
||||||
php spark migrate
|
php spark migrate
|
||||||
php spark migrate:rollback
|
php spark migrate:rollback
|
||||||
```
|
|
||||||
|
|
||||||
---
|
# After OpenAPI edits
|
||||||
|
|
||||||
## 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
|
node public/bundle-api-docs.js
|
||||||
```
|
```
|
||||||
|
|
||||||
This produces `public/api-docs.bundled.yaml` which is used by Swagger UI/Redoc.
|
Use `php spark test --filter <Class>::<method>` when filtering more than one test file is cumbersome.
|
||||||
|
|
||||||
### 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
|
## Agent Rules Scan
|
||||||
Database uses PascalCase columns: `PatientID`, `NameFirst`, `Birthdate`, `CreatedAt`, `UpdatedAt`
|
- 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.
|
||||||
|
|
||||||
### ValueSet System
|
---
|
||||||
```php
|
|
||||||
use App\Libraries\Lookups;
|
|
||||||
|
|
||||||
$genders = Lookups::get('gender');
|
## Coding Standards
|
||||||
$label = Lookups::getLabel('gender', '1'); // Returns 'Female'
|
|
||||||
$options = Lookups::getOptions('gender');
|
### Language & Formatting
|
||||||
$labeled = Lookups::transformLabels($data, ['Sex' => 'gender']);
|
- 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
|
### Nested Data Handling
|
||||||
For entities with nested data (PatIdt, PatCom, PatAtt):
|
- For entities that carry related collections (`PatIdt`, `PatCom`, `PatAtt`), extract nested arrays before filtering and validating.
|
||||||
- Extract nested arrays before filtering
|
- Use transactions whenever multi-table inserts/updates occur so orphan rows are avoided.
|
||||||
- Use transactions for multi-table operations
|
- Guard against empty/null arrays by normalizing to `[]` before iterating.
|
||||||
- Handle empty/null arrays appropriately
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Environment Configuration
|
## Final Notes for Agents
|
||||||
|
- This repo has no UI layer; focus exclusively on REST interactions.
|
||||||
### Database (`.env`)
|
- Always pull `public/api-docs.bundled.yaml` in after running `node public/bundle-api-docs.js` so downstream services see the latest contract.
|
||||||
```ini
|
- When in doubt, align with existing controller traits and response helpers to avoid duplicating logic.
|
||||||
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.*
|
|
||||||
|
|||||||
14
README.md
14
README.md
@ -158,9 +158,9 @@ All API endpoints follow REST conventions:
|
|||||||
|
|
||||||
| Method | Endpoint | Description | Auth Required |
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|--------|----------|-------------|---------------|
|
|--------|----------|-------------|---------------|
|
||||||
| `POST` | `/api/edge/results` | Receive instrument results | API Key |
|
| `POST` | `/api/edge/result` | Receive instrument results | API Key |
|
||||||
| `GET` | `/api/edge/orders` | Fetch pending orders | API Key |
|
| `GET` | `/api/edge/order` | Fetch pending orders | API Key |
|
||||||
| `POST` | `/api/edge/orders/{id}/ack` | Acknowledge order | API Key |
|
| `POST` | `/api/edge/order/{id}/ack` | Acknowledge order | API Key |
|
||||||
| `POST` | `/api/edge/status` | Log instrument status | API Key |
|
| `POST` | `/api/edge/status` | Log instrument status | API Key |
|
||||||
|
|
||||||
### API Response Format
|
### API Response Format
|
||||||
@ -524,15 +524,15 @@ The **Edge API** provides endpoints for integrating laboratory instruments via t
|
|||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `POST` | `/api/edge/results` | Receive instrument results (stored in `edgeres`) |
|
| `POST` | `/api/edge/result` | Receive instrument results (stored in `edgeres`) |
|
||||||
| `GET` | `/api/edge/orders` | Fetch pending orders for an instrument |
|
| `GET` | `/api/edge/order` | Fetch pending orders for an instrument |
|
||||||
| `POST` | `/api/edge/orders/:id/ack` | Acknowledge order delivery to instrument |
|
| `POST` | `/api/edge/order/:id/ack` | Acknowledge order delivery to instrument |
|
||||||
| `POST` | `/api/edge/status` | Log instrument status updates |
|
| `POST` | `/api/edge/status` | Log instrument status updates |
|
||||||
|
|
||||||
### Workflow
|
### 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:**
|
**Key Features:**
|
||||||
|
|||||||
@ -174,7 +174,7 @@ class Database extends Config
|
|||||||
'hostname' => 'localhost',
|
'hostname' => 'localhost',
|
||||||
'username' => 'root',
|
'username' => 'root',
|
||||||
'password' => 'adminsakti',
|
'password' => 'adminsakti',
|
||||||
'database' => 'clqms01',
|
'database' => 'clqms01_test',
|
||||||
'DBDriver' => 'MySQLi',
|
'DBDriver' => 'MySQLi',
|
||||||
'DBPrefix' => '', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
|
'DBPrefix' => '', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
|
||||||
'pConnect' => false,
|
'pConnect' => false,
|
||||||
@ -204,6 +204,9 @@ class Database extends Config
|
|||||||
// we are currently running an automated test suite, so that
|
// we are currently running an automated test suite, so that
|
||||||
// we don't overwrite live data on accident.
|
// we don't overwrite live data on accident.
|
||||||
if (ENVIRONMENT === 'testing') {
|
if (ENVIRONMENT === 'testing') {
|
||||||
|
if ($this->tests['database'] === $this->default['database']) {
|
||||||
|
throw new \RuntimeException('Tests database cannot match the default database.');
|
||||||
|
}
|
||||||
$this->defaultGroup = 'tests';
|
$this->defaultGroup = 'tests';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) {
|
|||||||
$routes->get('sample', 'SampleController::index');
|
$routes->get('sample', 'SampleController::index');
|
||||||
|
|
||||||
// Results CRUD
|
// Results CRUD
|
||||||
$routes->group('results', function ($routes) {
|
$routes->group('result', function ($routes) {
|
||||||
$routes->get('/', 'ResultController::index');
|
$routes->get('/', 'ResultController::index');
|
||||||
$routes->get('(:num)', 'ResultController::show/$1');
|
$routes->get('(:num)', 'ResultController::show/$1');
|
||||||
$routes->patch('(:num)', 'ResultController::update/$1');
|
$routes->patch('(:num)', 'ResultController::update/$1');
|
||||||
@ -26,7 +26,7 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Reports
|
// Reports
|
||||||
$routes->get('reports/(:num)', 'ReportController::view/$1');
|
$routes->get('report/(:num)', 'ReportController::view/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->post('/', 'Patient\PatientController::create');
|
$routes->post('/', 'Patient\PatientController::create');
|
||||||
$routes->get('(:num)', 'Patient\PatientController::show/$1');
|
$routes->get('(:num)', 'Patient\PatientController::show/$1');
|
||||||
$routes->delete('/', 'Patient\PatientController::delete');
|
$routes->delete('/', 'Patient\PatientController::delete');
|
||||||
$routes->patch('/', 'Patient\PatientController::update');
|
$routes->patch('(:num)', 'Patient\PatientController::update/$1');
|
||||||
$routes->get('check', 'Patient\PatientController::patientCheck');
|
$routes->get('check', 'Patient\PatientController::patientCheck');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,14 +69,14 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('patient/(:num)', 'PatVisitController::showByPatient/$1');
|
$routes->get('patient/(:num)', 'PatVisitController::showByPatient/$1');
|
||||||
$routes->get('(:any)', 'PatVisitController::show/$1');
|
$routes->get('(:any)', 'PatVisitController::show/$1');
|
||||||
$routes->delete('/', 'PatVisitController::delete');
|
$routes->delete('/', 'PatVisitController::delete');
|
||||||
$routes->patch('/', 'PatVisitController::update');
|
$routes->patch('(:any)', 'PatVisitController::update/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('patvisitadt', function ($routes) {
|
$routes->group('patvisitadt', function ($routes) {
|
||||||
$routes->get('visit/(:num)', 'PatVisitController::getADTByVisit/$1');
|
$routes->get('visit/(:num)', 'PatVisitController::getADTByVisit/$1');
|
||||||
$routes->get('(:num)', 'PatVisitController::showADT/$1');
|
$routes->get('(:num)', 'PatVisitController::showADT/$1');
|
||||||
$routes->post('/', 'PatVisitController::createADT');
|
$routes->post('/', 'PatVisitController::createADT');
|
||||||
$routes->patch('/', 'PatVisitController::updateADT');
|
$routes->patch('(:num)', 'PatVisitController::updateADT/$1');
|
||||||
$routes->delete('/', 'PatVisitController::deleteADT');
|
$routes->delete('/', 'PatVisitController::deleteADT');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'LocationController::index');
|
$routes->get('/', 'LocationController::index');
|
||||||
$routes->get('(:num)', 'LocationController::show/$1');
|
$routes->get('(:num)', 'LocationController::show/$1');
|
||||||
$routes->post('/', 'LocationController::create');
|
$routes->post('/', 'LocationController::create');
|
||||||
$routes->patch('/', 'LocationController::update');
|
$routes->patch('(:num)', 'LocationController::update/$1');
|
||||||
$routes->delete('/', 'LocationController::delete');
|
$routes->delete('/', 'LocationController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Contact\ContactController::index');
|
$routes->get('/', 'Contact\ContactController::index');
|
||||||
$routes->get('(:num)', 'Contact\ContactController::show/$1');
|
$routes->get('(:num)', 'Contact\ContactController::show/$1');
|
||||||
$routes->post('/', 'Contact\ContactController::create');
|
$routes->post('/', 'Contact\ContactController::create');
|
||||||
$routes->patch('/', 'Contact\ContactController::update');
|
$routes->patch('(:num)', 'Contact\ContactController::update/$1');
|
||||||
$routes->delete('/', 'Contact\ContactController::delete');
|
$routes->delete('/', 'Contact\ContactController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Contact\OccupationController::index');
|
$routes->get('/', 'Contact\OccupationController::index');
|
||||||
$routes->get('(:num)', 'Contact\OccupationController::show/$1');
|
$routes->get('(:num)', 'Contact\OccupationController::show/$1');
|
||||||
$routes->post('/', 'Contact\OccupationController::create');
|
$routes->post('/', 'Contact\OccupationController::create');
|
||||||
$routes->patch('/', 'Contact\OccupationController::update');
|
$routes->patch('(:num)', 'Contact\OccupationController::update/$1');
|
||||||
//$routes->delete('/', 'Contact\OccupationController::delete');
|
//$routes->delete('/', 'Contact\OccupationController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Contact\MedicalSpecialtyController::index');
|
$routes->get('/', 'Contact\MedicalSpecialtyController::index');
|
||||||
$routes->get('(:num)', 'Contact\MedicalSpecialtyController::show/$1');
|
$routes->get('(:num)', 'Contact\MedicalSpecialtyController::show/$1');
|
||||||
$routes->post('/', 'Contact\MedicalSpecialtyController::create');
|
$routes->post('/', 'Contact\MedicalSpecialtyController::create');
|
||||||
$routes->patch('/', 'Contact\MedicalSpecialtyController::update');
|
$routes->patch('(:num)', 'Contact\MedicalSpecialtyController::update/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lib ValueSet (file-based)
|
// Lib ValueSet (file-based)
|
||||||
@ -159,7 +159,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'CounterController::index');
|
$routes->get('/', 'CounterController::index');
|
||||||
$routes->get('(:num)', 'CounterController::show/$1');
|
$routes->get('(:num)', 'CounterController::show/$1');
|
||||||
$routes->post('/', 'CounterController::create');
|
$routes->post('/', 'CounterController::create');
|
||||||
$routes->patch('/', 'CounterController::update');
|
$routes->patch('(:num)', 'CounterController::update/$1');
|
||||||
$routes->delete('/', 'CounterController::delete');
|
$routes->delete('/', 'CounterController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\AccountController::index');
|
$routes->get('/', 'Organization\AccountController::index');
|
||||||
$routes->get('(:num)', 'Organization\AccountController::show/$1');
|
$routes->get('(:num)', 'Organization\AccountController::show/$1');
|
||||||
$routes->post('/', 'Organization\AccountController::create');
|
$routes->post('/', 'Organization\AccountController::create');
|
||||||
$routes->patch('/', 'Organization\AccountController::update');
|
$routes->patch('(:num)', 'Organization\AccountController::update/$1');
|
||||||
$routes->delete('/', 'Organization\AccountController::delete');
|
$routes->delete('/', 'Organization\AccountController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\SiteController::index');
|
$routes->get('/', 'Organization\SiteController::index');
|
||||||
$routes->get('(:num)', 'Organization\SiteController::show/$1');
|
$routes->get('(:num)', 'Organization\SiteController::show/$1');
|
||||||
$routes->post('/', 'Organization\SiteController::create');
|
$routes->post('/', 'Organization\SiteController::create');
|
||||||
$routes->patch('/', 'Organization\SiteController::update');
|
$routes->patch('(:num)', 'Organization\SiteController::update/$1');
|
||||||
$routes->delete('/', 'Organization\SiteController::delete');
|
$routes->delete('/', 'Organization\SiteController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\DisciplineController::index');
|
$routes->get('/', 'Organization\DisciplineController::index');
|
||||||
$routes->get('(:num)', 'Organization\DisciplineController::show/$1');
|
$routes->get('(:num)', 'Organization\DisciplineController::show/$1');
|
||||||
$routes->post('/', 'Organization\DisciplineController::create');
|
$routes->post('/', 'Organization\DisciplineController::create');
|
||||||
$routes->patch('/', 'Organization\DisciplineController::update');
|
$routes->patch('(:num)', 'Organization\DisciplineController::update/$1');
|
||||||
$routes->delete('/', 'Organization\DisciplineController::delete');
|
$routes->delete('/', 'Organization\DisciplineController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,7 +204,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\DepartmentController::index');
|
$routes->get('/', 'Organization\DepartmentController::index');
|
||||||
$routes->get('(:num)', 'Organization\DepartmentController::show/$1');
|
$routes->get('(:num)', 'Organization\DepartmentController::show/$1');
|
||||||
$routes->post('/', 'Organization\DepartmentController::create');
|
$routes->post('/', 'Organization\DepartmentController::create');
|
||||||
$routes->patch('/', 'Organization\DepartmentController::update');
|
$routes->patch('(:num)', 'Organization\DepartmentController::update/$1');
|
||||||
$routes->delete('/', 'Organization\DepartmentController::delete');
|
$routes->delete('/', 'Organization\DepartmentController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -213,7 +213,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\WorkstationController::index');
|
$routes->get('/', 'Organization\WorkstationController::index');
|
||||||
$routes->get('(:num)', 'Organization\WorkstationController::show/$1');
|
$routes->get('(:num)', 'Organization\WorkstationController::show/$1');
|
||||||
$routes->post('/', 'Organization\WorkstationController::create');
|
$routes->post('/', 'Organization\WorkstationController::create');
|
||||||
$routes->patch('/', 'Organization\WorkstationController::update');
|
$routes->patch('(:num)', 'Organization\WorkstationController::update/$1');
|
||||||
$routes->delete('/', 'Organization\WorkstationController::delete');
|
$routes->delete('/', 'Organization\WorkstationController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\HostAppController::index');
|
$routes->get('/', 'Organization\HostAppController::index');
|
||||||
$routes->get('(:any)', 'Organization\HostAppController::show/$1');
|
$routes->get('(:any)', 'Organization\HostAppController::show/$1');
|
||||||
$routes->post('/', 'Organization\HostAppController::create');
|
$routes->post('/', 'Organization\HostAppController::create');
|
||||||
$routes->patch('/', 'Organization\HostAppController::update');
|
$routes->patch('(:any)', 'Organization\HostAppController::update/$1');
|
||||||
$routes->delete('/', 'Organization\HostAppController::delete');
|
$routes->delete('/', 'Organization\HostAppController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\HostComParaController::index');
|
$routes->get('/', 'Organization\HostComParaController::index');
|
||||||
$routes->get('(:any)', 'Organization\HostComParaController::show/$1');
|
$routes->get('(:any)', 'Organization\HostComParaController::show/$1');
|
||||||
$routes->post('/', 'Organization\HostComParaController::create');
|
$routes->post('/', 'Organization\HostComParaController::create');
|
||||||
$routes->patch('/', 'Organization\HostComParaController::update');
|
$routes->patch('(:any)', 'Organization\HostComParaController::update/$1');
|
||||||
$routes->delete('/', 'Organization\HostComParaController::delete');
|
$routes->delete('/', 'Organization\HostComParaController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -240,7 +240,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Organization\CodingSysController::index');
|
$routes->get('/', 'Organization\CodingSysController::index');
|
||||||
$routes->get('(:num)', 'Organization\CodingSysController::show/$1');
|
$routes->get('(:num)', 'Organization\CodingSysController::show/$1');
|
||||||
$routes->post('/', 'Organization\CodingSysController::create');
|
$routes->post('/', 'Organization\CodingSysController::create');
|
||||||
$routes->patch('/', 'Organization\CodingSysController::update');
|
$routes->patch('(:num)', 'Organization\CodingSysController::update/$1');
|
||||||
$routes->delete('/', 'Organization\CodingSysController::delete');
|
$routes->delete('/', 'Organization\CodingSysController::delete');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -250,16 +250,16 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Infrastructure\EquipmentListController::index');
|
$routes->get('/', 'Infrastructure\EquipmentListController::index');
|
||||||
$routes->get('(:num)', 'Infrastructure\EquipmentListController::show/$1');
|
$routes->get('(:num)', 'Infrastructure\EquipmentListController::show/$1');
|
||||||
$routes->post('/', 'Infrastructure\EquipmentListController::create');
|
$routes->post('/', 'Infrastructure\EquipmentListController::create');
|
||||||
$routes->patch('/', 'Infrastructure\EquipmentListController::update');
|
$routes->patch('(:num)', 'Infrastructure\EquipmentListController::update/$1');
|
||||||
$routes->delete('/', 'Infrastructure\EquipmentListController::delete');
|
$routes->delete('/', 'Infrastructure\EquipmentListController::delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
$routes->group('users', function ($routes) {
|
$routes->group('user', function ($routes) {
|
||||||
$routes->get('/', 'User\UserController::index');
|
$routes->get('/', 'User\UserController::index');
|
||||||
$routes->get('(:num)', 'User\UserController::show/$1');
|
$routes->get('(:num)', 'User\UserController::show/$1');
|
||||||
$routes->post('/', 'User\UserController::create');
|
$routes->post('/', 'User\UserController::create');
|
||||||
$routes->patch('/', 'User\UserController::update');
|
$routes->patch('(:num)', 'User\UserController::update/$1');
|
||||||
$routes->delete('(:num)', 'User\UserController::delete/$1');
|
$routes->delete('(:num)', 'User\UserController::delete/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -270,40 +270,40 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Specimen\ContainerDefController::index');
|
$routes->get('/', 'Specimen\ContainerDefController::index');
|
||||||
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
|
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
|
||||||
$routes->post('/', 'Specimen\ContainerDefController::create');
|
$routes->post('/', 'Specimen\ContainerDefController::create');
|
||||||
$routes->patch('/', 'Specimen\ContainerDefController::update');
|
$routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1');
|
||||||
});
|
});
|
||||||
$routes->group('containerdef', function ($routes) {
|
$routes->group('containerdef', function ($routes) {
|
||||||
$routes->get('/', 'Specimen\ContainerDefController::index');
|
$routes->get('/', 'Specimen\ContainerDefController::index');
|
||||||
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
|
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
|
||||||
$routes->post('/', 'Specimen\ContainerDefController::create');
|
$routes->post('/', 'Specimen\ContainerDefController::create');
|
||||||
$routes->patch('/', 'Specimen\ContainerDefController::update');
|
$routes->patch('(:num)', 'Specimen\ContainerDefController::update/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('prep', function ($routes) {
|
$routes->group('prep', function ($routes) {
|
||||||
$routes->get('/', 'Specimen\SpecimenPrepController::index');
|
$routes->get('/', 'Specimen\SpecimenPrepController::index');
|
||||||
$routes->get('(:num)', 'Specimen\SpecimenPrepController::show/$1');
|
$routes->get('(:num)', 'Specimen\SpecimenPrepController::show/$1');
|
||||||
$routes->post('/', 'Specimen\SpecimenPrepController::create');
|
$routes->post('/', 'Specimen\SpecimenPrepController::create');
|
||||||
$routes->patch('/', 'Specimen\SpecimenPrepController::update');
|
$routes->patch('(:num)', 'Specimen\SpecimenPrepController::update/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('status', function ($routes) {
|
$routes->group('status', function ($routes) {
|
||||||
$routes->get('/', 'Specimen\SpecimenStatusController::index');
|
$routes->get('/', 'Specimen\SpecimenStatusController::index');
|
||||||
$routes->get('(:num)', 'Specimen\SpecimenStatusController::show/$1');
|
$routes->get('(:num)', 'Specimen\SpecimenStatusController::show/$1');
|
||||||
$routes->post('/', 'Specimen\SpecimenStatusController::create');
|
$routes->post('/', 'Specimen\SpecimenStatusController::create');
|
||||||
$routes->patch('/', 'Specimen\SpecimenStatusController::update');
|
$routes->patch('(:num)', 'Specimen\SpecimenStatusController::update/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('collection', function ($routes) {
|
$routes->group('collection', function ($routes) {
|
||||||
$routes->get('/', 'Specimen\SpecimenCollectionController::index');
|
$routes->get('/', 'Specimen\SpecimenCollectionController::index');
|
||||||
$routes->get('(:num)', 'Specimen\SpecimenCollectionController::show/$1');
|
$routes->get('(:num)', 'Specimen\SpecimenCollectionController::show/$1');
|
||||||
$routes->post('/', 'Specimen\SpecimenCollectionController::create');
|
$routes->post('/', 'Specimen\SpecimenCollectionController::create');
|
||||||
$routes->patch('/', 'Specimen\SpecimenCollectionController::update');
|
$routes->patch('(:num)', 'Specimen\SpecimenCollectionController::update/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->get('/', 'Specimen\SpecimenController::index');
|
$routes->get('/', 'Specimen\SpecimenController::index');
|
||||||
$routes->get('(:num)', 'Specimen\SpecimenController::show/$1');
|
$routes->get('(:num)', 'Specimen\SpecimenController::show/$1');
|
||||||
$routes->post('/', 'Specimen\SpecimenController::create');
|
$routes->post('/', 'Specimen\SpecimenController::create');
|
||||||
$routes->patch('/', 'Specimen\SpecimenController::update');
|
$routes->patch('(:num)', 'Specimen\SpecimenController::update/$1');
|
||||||
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
|
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -312,12 +312,12 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Test\TestsController::index');
|
$routes->get('/', 'Test\TestsController::index');
|
||||||
$routes->get('(:num)', 'Test\TestsController::show/$1');
|
$routes->get('(:num)', 'Test\TestsController::show/$1');
|
||||||
$routes->post('/', 'Test\TestsController::create');
|
$routes->post('/', 'Test\TestsController::create');
|
||||||
$routes->patch('/', 'Test\TestsController::update');
|
$routes->patch('(:num)', 'Test\TestsController::update/$1');
|
||||||
$routes->group('testmap', function ($routes) {
|
$routes->group('testmap', function ($routes) {
|
||||||
$routes->get('/', 'Test\TestMapController::index');
|
$routes->get('/', 'Test\TestMapController::index');
|
||||||
$routes->get('(:num)', 'Test\TestMapController::show/$1');
|
$routes->get('(:num)', 'Test\TestMapController::show/$1');
|
||||||
$routes->post('/', 'Test\TestMapController::create');
|
$routes->post('/', 'Test\TestMapController::create');
|
||||||
$routes->patch('/', 'Test\TestMapController::update');
|
$routes->patch('(:num)', 'Test\TestMapController::update/$1');
|
||||||
$routes->delete('/', 'Test\TestMapController::delete');
|
$routes->delete('/', 'Test\TestMapController::delete');
|
||||||
|
|
||||||
// Filter routes
|
// Filter routes
|
||||||
@ -328,7 +328,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'Test\TestMapDetailController::index');
|
$routes->get('/', 'Test\TestMapDetailController::index');
|
||||||
$routes->get('(:num)', 'Test\TestMapDetailController::show/$1');
|
$routes->get('(:num)', 'Test\TestMapDetailController::show/$1');
|
||||||
$routes->post('/', 'Test\TestMapDetailController::create');
|
$routes->post('/', 'Test\TestMapDetailController::create');
|
||||||
$routes->patch('/', 'Test\TestMapDetailController::update');
|
$routes->patch('(:num)', 'Test\TestMapDetailController::update/$1');
|
||||||
$routes->delete('/', 'Test\TestMapDetailController::delete');
|
$routes->delete('/', 'Test\TestMapDetailController::delete');
|
||||||
$routes->get('by-testmap/(:num)', 'Test\TestMapDetailController::showByTestMap/$1');
|
$routes->get('by-testmap/(:num)', 'Test\TestMapDetailController::showByTestMap/$1');
|
||||||
$routes->post('batch', 'Test\TestMapDetailController::batchCreate');
|
$routes->post('batch', 'Test\TestMapDetailController::batchCreate');
|
||||||
@ -343,13 +343,13 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('/', 'OrderTestController::index');
|
$routes->get('/', 'OrderTestController::index');
|
||||||
$routes->get('(:any)', 'OrderTestController::show/$1');
|
$routes->get('(:any)', 'OrderTestController::show/$1');
|
||||||
$routes->post('/', 'OrderTestController::create');
|
$routes->post('/', 'OrderTestController::create');
|
||||||
$routes->patch('/', 'OrderTestController::update');
|
$routes->patch('(:any)', 'OrderTestController::update/$1');
|
||||||
$routes->delete('/', 'OrderTestController::delete');
|
$routes->delete('/', 'OrderTestController::delete');
|
||||||
$routes->post('status', 'OrderTestController::updateStatus');
|
$routes->post('status', 'OrderTestController::updateStatus');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rules
|
// Rules
|
||||||
$routes->group('rules', function ($routes) {
|
$routes->group('rule', function ($routes) {
|
||||||
$routes->get('/', 'Rule\RuleController::index');
|
$routes->get('/', 'Rule\RuleController::index');
|
||||||
$routes->get('(:num)', 'Rule\RuleController::show/$1');
|
$routes->get('(:num)', 'Rule\RuleController::show/$1');
|
||||||
$routes->post('/', 'Rule\RuleController::create');
|
$routes->post('/', 'Rule\RuleController::create');
|
||||||
@ -362,14 +362,14 @@ $routes->group('api', function ($routes) {
|
|||||||
// Demo/Test Routes (No Auth)
|
// Demo/Test Routes (No Auth)
|
||||||
$routes->group('api/demo', function ($routes) {
|
$routes->group('api/demo', function ($routes) {
|
||||||
$routes->post('order', 'Test\DemoOrderController::createDemoOrder');
|
$routes->post('order', 'Test\DemoOrderController::createDemoOrder');
|
||||||
$routes->get('orders', 'Test\DemoOrderController::listDemoOrders');
|
$routes->get('order', 'Test\DemoOrderController::listDemoOrders');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Edge API - Integration with tiny-edge
|
// Edge API - Integration with tiny-edge
|
||||||
$routes->group('edge', function ($routes) {
|
$routes->group('edge', function ($routes) {
|
||||||
$routes->post('results', 'EdgeController::results');
|
$routes->post('result', 'EdgeController::results');
|
||||||
$routes->get('orders', 'EdgeController::orders');
|
$routes->get('order', 'EdgeController::orders');
|
||||||
$routes->post('orders/(:num)/ack', 'EdgeController::ack/$1');
|
$routes->post('order/(:num)/ack', 'EdgeController::ack/$1');
|
||||||
$routes->post('status', 'EdgeController::status');
|
$routes->post('status', 'EdgeController::status');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -75,8 +75,16 @@ class ContactController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($ContactID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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()); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||||
try {
|
try {
|
||||||
$this->model->saveContact($input);
|
$this->model->saveContact($input);
|
||||||
|
|||||||
@ -51,8 +51,12 @@ class MedicalSpecialtyController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($SpecialtyID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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 {
|
try {
|
||||||
$this->model->update($input['SpecialtyID'], $input);
|
$this->model->update($input['SpecialtyID'], $input);
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['SpecialtyID'] ], 201);
|
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['SpecialtyID'] ], 201);
|
||||||
|
|||||||
@ -51,8 +51,12 @@ class OccupationController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($OccupationID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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 {
|
try {
|
||||||
$this->model->update($input['OccupationID'], $input);
|
$this->model->update($input['OccupationID'], $input);
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['OccupationID'] ], 201);
|
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['OccupationID'] ], 201);
|
||||||
|
|||||||
@ -43,8 +43,12 @@ class CounterController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($CounterID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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 {
|
try {
|
||||||
$this->model->update($input['CounterID'], $input);
|
$this->model->update($input['CounterID'], $input);
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['CounterID'] ], 201);
|
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => $input['CounterID'] ], 201);
|
||||||
|
|||||||
@ -19,7 +19,7 @@ class EdgeController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/edge/results
|
* POST /api/edge/result
|
||||||
* Receive results from tiny-edge
|
* Receive results from tiny-edge
|
||||||
*/
|
*/
|
||||||
public function results()
|
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
|
* Return pending orders for an instrument
|
||||||
*/
|
*/
|
||||||
public function orders()
|
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
|
* Acknowledge order delivery
|
||||||
*/
|
*/
|
||||||
public function ack($orderId = null)
|
public function ack($orderId = null)
|
||||||
|
|||||||
@ -76,11 +76,14 @@ class EquipmentListController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($EID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$EID = $input['EID'];
|
if (!$EID || !ctype_digit((string) $EID)) {
|
||||||
|
return $this->failValidationErrors('EID is required.');
|
||||||
|
}
|
||||||
|
$input['EID'] = (int) $EID;
|
||||||
$this->model->update($EID, $input);
|
$this->model->update($EID, $input);
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
|
|||||||
@ -50,8 +50,16 @@ class LocationController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($LocationID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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 {
|
try {
|
||||||
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors()); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors()); }
|
||||||
$result = $this->model->saveLocation($input, true);
|
$result = $this->model->saveLocation($input, true);
|
||||||
|
|||||||
@ -191,15 +191,20 @@ class OrderTestController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($OrderID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
if (empty($input['OrderID'])) {
|
if ($OrderID === null || $OrderID === '') {
|
||||||
return $this->failValidationErrors(['OrderID' => 'OrderID is required']);
|
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 {
|
try {
|
||||||
$order = $this->model->getOrder($input['OrderID']);
|
$input['OrderID'] = $OrderID;
|
||||||
|
$order = $this->model->getOrder($OrderID);
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
return $this->failNotFound('Order not found');
|
return $this->failNotFound('Order not found');
|
||||||
}
|
}
|
||||||
@ -215,7 +220,7 @@ class OrderTestController extends Controller {
|
|||||||
$this->model->update($order['InternalOID'], $updateData);
|
$this->model->update($order['InternalOID'], $updateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
$updatedOrder = $this->model->getOrder($input['OrderID']);
|
$updatedOrder = $this->model->getOrder($OrderID);
|
||||||
$updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']);
|
$updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']);
|
||||||
$updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']);
|
$updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']);
|
||||||
|
|
||||||
|
|||||||
@ -65,8 +65,12 @@ class AccountController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($AccountID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
if (!$AccountID || !ctype_digit((string) $AccountID)) {
|
||||||
|
return $this->failValidationErrors('ID is required.');
|
||||||
|
}
|
||||||
|
$input['AccountID'] = (int) $AccountID;
|
||||||
try {
|
try {
|
||||||
$id = $input['AccountID'];
|
$id = $input['AccountID'];
|
||||||
if (!$id) { return $this->failValidationErrors('ID is required.'); }
|
if (!$id) { return $this->failValidationErrors('ID is required.'); }
|
||||||
|
|||||||
@ -78,16 +78,21 @@ class CodingSysController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($CodingSysID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$id = $input['CodingSysID'] ?? null;
|
if (!$CodingSysID || !ctype_digit((string) $CodingSysID)) {
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
return $this->failValidationErrors('CodingSysID is required.');
|
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);
|
$this->model->update($id, $input);
|
||||||
return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201);
|
return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
@ -62,9 +62,13 @@ class DepartmentController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($DepartmentID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
try {
|
try {
|
||||||
|
if (!$DepartmentID || !ctype_digit((string) $DepartmentID)) {
|
||||||
|
return $this->failValidationErrors('ID is required.');
|
||||||
|
}
|
||||||
|
$input['DepartmentID'] = (int) $DepartmentID;
|
||||||
$id = $input['DepartmentID'];
|
$id = $input['DepartmentID'];
|
||||||
$this->model->update($id, $input);
|
$this->model->update($id, $input);
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201);
|
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201);
|
||||||
|
|||||||
@ -63,8 +63,10 @@ class DisciplineController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($DisciplineID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
if (!$DisciplineID || !ctype_digit((string) $DisciplineID)) { return $this->failValidationErrors('ID is required.'); }
|
||||||
|
$input['DisciplineID'] = (int) $DisciplineID;
|
||||||
$id = $input['DisciplineID'];
|
$id = $input['DisciplineID'];
|
||||||
$this->model->update($id, $input);
|
$this->model->update($id, $input);
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201);
|
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201);
|
||||||
|
|||||||
@ -82,16 +82,20 @@ class HostAppController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($HostAppID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$id = $input['HostAppID'] ?? null;
|
if ($HostAppID === null || $HostAppID === '') {
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
return $this->failValidationErrors('HostAppID is required.');
|
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);
|
$this->model->update($id, $input);
|
||||||
return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201);
|
return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
@ -82,16 +82,21 @@ class HostComParaController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($HostAppID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$id = $input['HostAppID'] ?? null;
|
if ($HostAppID === null || $HostAppID === '') {
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
return $this->failValidationErrors('HostAppID is required.');
|
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);
|
$this->model->update($id, $input);
|
||||||
return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201);
|
return $this->respondCreated(['status' => 'success', 'message' => 'data updated successfully', 'data' => $id], 201);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
@ -59,7 +59,7 @@ class SiteController extends BaseController {
|
|||||||
|
|
||||||
$validation = service('validation');
|
$validation = service('validation');
|
||||||
$validation->setRules([
|
$validation->setRules([
|
||||||
'SiteCode' => 'required|regex_match[/^[A-Z0-9]{2}$/]',
|
'SiteCode' => 'required|regex_match[/^[A-Z0-9]{2,6}$/]',
|
||||||
'SiteName' => 'required',
|
'SiteName' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -75,16 +75,18 @@ class SiteController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($SiteID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
|
if (!$SiteID || !ctype_digit((string) $SiteID)) { return $this->failValidationErrors('ID is required.'); }
|
||||||
|
$input['SiteID'] = (int) $SiteID;
|
||||||
|
|
||||||
$id = $input['SiteID'];
|
$id = $input['SiteID'];
|
||||||
if (!$id) { return $this->failValidationErrors('ID is required.'); }
|
|
||||||
|
|
||||||
if (!empty($input['SiteCode'])) {
|
if (!empty($input['SiteCode'])) {
|
||||||
$validation = service('validation');
|
$validation = service('validation');
|
||||||
$validation->setRules([
|
$validation->setRules([
|
||||||
'SiteCode' => 'regex_match[/^[A-Z0-9]{2}$/]',
|
'SiteCode' => 'regex_match[/^[A-Z0-9]{2,6}$/]',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$validation->run($input)) {
|
if (!$validation->run($input)) {
|
||||||
|
|||||||
@ -63,9 +63,13 @@ class WorkstationController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($WorkstationID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
try {
|
try {
|
||||||
|
if (!$WorkstationID || !ctype_digit((string) $WorkstationID)) {
|
||||||
|
return $this->failValidationErrors('ID is required.');
|
||||||
|
}
|
||||||
|
$input['WorkstationID'] = (int) $WorkstationID;
|
||||||
$id = $input['WorkstationID'];
|
$id = $input['WorkstationID'];
|
||||||
$this->model->update($id, $input);
|
$this->model->update($id, $input);
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201);
|
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201);
|
||||||
|
|||||||
@ -94,13 +94,13 @@ class PatVisitController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($InternalPVID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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 {
|
try {
|
||||||
if (!isset($input["InternalPVID"]) || !is_numeric($input["InternalPVID"])) {
|
|
||||||
return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if visit exists
|
// Check if visit exists
|
||||||
$visit = $this->model->find($input["InternalPVID"]);
|
$visit = $this->model->find($input["InternalPVID"]);
|
||||||
if (!$visit) {
|
if (!$visit) {
|
||||||
@ -174,9 +174,10 @@ class PatVisitController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateADT() {
|
public function updateADT($PVADTID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
if (!$input["PVADTID"] || !is_numeric($input["PVADTID"])) { return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); }
|
if (!$PVADTID || !is_numeric($PVADTID)) { return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); }
|
||||||
|
$input['PVADTID'] = $PVADTID;
|
||||||
$modelPVA = new PatVisitADTModel();
|
$modelPVA = new PatVisitADTModel();
|
||||||
try {
|
try {
|
||||||
$data = $modelPVA->update($input['PVADTID'], $input);
|
$data = $modelPVA->update($input['PVADTID'], $input);
|
||||||
|
|||||||
@ -115,9 +115,18 @@ class PatientController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($InternalPID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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
|
// Khusus untuk Override PATIDT
|
||||||
$type = $input['PatIdt']['IdentifierType'] ?? null;
|
$type = $input['PatIdt']['IdentifierType'] ?? null;
|
||||||
$identifierRulesMap = [
|
$identifierRulesMap = [
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class ReportController extends Controller {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate HTML lab report for an order
|
* Generate HTML lab report for an order
|
||||||
* GET /api/reports/{orderID}
|
* GET /api/report/{orderID}
|
||||||
*/
|
*/
|
||||||
public function view($orderID) {
|
public function view($orderID) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class ResultController extends Controller {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List results with optional filters
|
* List results with optional filters
|
||||||
* GET /api/results
|
* GET /api/result
|
||||||
*/
|
*/
|
||||||
public function index() {
|
public function index() {
|
||||||
try {
|
try {
|
||||||
@ -57,7 +57,7 @@ class ResultController extends Controller {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get single result
|
* Get single result
|
||||||
* GET /api/results/{id}
|
* GET /api/result/{id}
|
||||||
*/
|
*/
|
||||||
public function show($id) {
|
public function show($id) {
|
||||||
try {
|
try {
|
||||||
@ -89,7 +89,7 @@ class ResultController extends Controller {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update result with validation
|
* Update result with validation
|
||||||
* PATCH /api/results/{id}
|
* PATCH /api/result/{id}
|
||||||
*/
|
*/
|
||||||
public function update($id) {
|
public function update($id) {
|
||||||
try {
|
try {
|
||||||
@ -137,7 +137,7 @@ class ResultController extends Controller {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Soft delete result
|
* Soft delete result
|
||||||
* DELETE /api/results/{id}
|
* DELETE /api/result/{id}
|
||||||
*/
|
*/
|
||||||
public function delete($id) {
|
public function delete($id) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -73,8 +73,12 @@ class ContainerDefController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($ConDefID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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()); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||||
try {
|
try {
|
||||||
$ConDefID = $this->model->update($input['ConDefID'], $input);
|
$ConDefID = $this->model->update($input['ConDefID'], $input);
|
||||||
|
|||||||
@ -66,8 +66,12 @@ class SpecimenCollectionController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($SpcColID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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()); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||||
try {
|
try {
|
||||||
$id = $this->model->update($input['SpcColID'], $input);
|
$id = $this->model->update($input['SpcColID'], $input);
|
||||||
|
|||||||
@ -66,8 +66,12 @@ class SpecimenController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($SID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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()); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||||
try {
|
try {
|
||||||
$id = $this->model->update($input['SID'], $input);
|
$id = $this->model->update($input['SID'], $input);
|
||||||
|
|||||||
@ -51,8 +51,12 @@ class SpecimenPrepController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($SpcPrpID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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()); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||||
try {
|
try {
|
||||||
$id = $this->model->update($input['SpcPrpID'], $input);
|
$id = $this->model->update($input['SpcPrpID'], $input);
|
||||||
|
|||||||
@ -64,8 +64,12 @@ class ContainerDef extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($SpcStaID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$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()); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||||
try {
|
try {
|
||||||
$id = $this->model->update($input['SpcStaID'], $input);
|
$id = $this->model->update($input['SpcStaID'], $input);
|
||||||
|
|||||||
@ -64,10 +64,14 @@ class TestMapController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($TestMapID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
$id = $input["TestMapID"];
|
if (!$TestMapID || !ctype_digit((string) $TestMapID)) { return $this->failValidationErrors('TestMapID is required.'); }
|
||||||
if (!$id) { 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() ); }
|
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors() ); }
|
||||||
try {
|
try {
|
||||||
$this->model->update($id,$input);
|
$this->model->update($id,$input);
|
||||||
|
|||||||
@ -89,13 +89,16 @@ class TestMapDetailController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update() {
|
public function update($TestMapDetailID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
$id = $input["TestMapDetailID"] ?? null;
|
if (!$TestMapDetailID || !ctype_digit((string) $TestMapDetailID)) {
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
return $this->failValidationErrors('TestMapDetailID is required.');
|
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)) {
|
if (!$this->validateData($input, $this->rules)) {
|
||||||
return $this->failValidationErrors($this->validator->getErrors());
|
return $this->failValidationErrors($this->validator->getErrors());
|
||||||
|
|||||||
@ -171,7 +171,10 @@ class TestsController extends BaseController
|
|||||||
|
|
||||||
$id = $this->model->insert($testSiteData);
|
$id = $this->model->insert($testSiteData);
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
throw new \Exception('Failed to insert main test definition');
|
$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');
|
$this->handleDetails($id, $input, 'insert');
|
||||||
@ -179,6 +182,12 @@ class TestsController extends BaseController
|
|||||||
$db->transComplete();
|
$db->transComplete();
|
||||||
|
|
||||||
if ($db->transStatus() === false) {
|
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->failServerError('Transaction failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List users with pagination and search
|
* 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()
|
public function index()
|
||||||
{
|
{
|
||||||
@ -81,7 +81,7 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get single user by ID
|
* Get single user by ID
|
||||||
* GET /api/users/(:num)
|
* GET /api/user/(:num)
|
||||||
*/
|
*/
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
@ -116,7 +116,7 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new user
|
* Create new user
|
||||||
* POST /api/users
|
* POST /api/user
|
||||||
*/
|
*/
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
@ -173,14 +173,14 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update existing user
|
* Update existing user
|
||||||
* PATCH /api/users
|
* PATCH /api/user/(:num)
|
||||||
*/
|
*/
|
||||||
public function update()
|
public function update($id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$data = $this->request->getJSON(true);
|
$data = $this->request->getJSON(true);
|
||||||
|
|
||||||
if (empty($data['UserID'])) {
|
if (empty($id) || !ctype_digit((string) $id)) {
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'failed',
|
'status' => 'failed',
|
||||||
'message' => 'UserID is required',
|
'message' => 'UserID is required',
|
||||||
@ -188,7 +188,15 @@ class UserController extends BaseController
|
|||||||
], 400);
|
], 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
|
// Check if user exists
|
||||||
$user = $this->model->where('UserID', $userId)
|
$user = $this->model->where('UserID', $userId)
|
||||||
@ -243,7 +251,7 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete user (soft delete)
|
* Delete user (soft delete)
|
||||||
* DELETE /api/users/(:num)
|
* DELETE /api/user/(:num)
|
||||||
*/
|
*/
|
||||||
public function delete($id)
|
public function delete($id)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -168,8 +168,12 @@ class TestValidationService
|
|||||||
* @param string $refType
|
* @param string $refType
|
||||||
* @return string|null Returns table name or null if no reference table needed
|
* @return string|null Returns table name or null if no reference table needed
|
||||||
*/
|
*/
|
||||||
public static function getReferenceTable(string $resultType, string $refType): ?string
|
public static function getReferenceTable(?string $resultType, ?string $refType): ?string
|
||||||
{
|
{
|
||||||
|
if ($resultType === null || $refType === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$resultType = strtoupper($resultType);
|
$resultType = strtoupper($resultType);
|
||||||
$refType = strtoupper($refType);
|
$refType = strtoupper($refType);
|
||||||
|
|
||||||
@ -182,8 +186,12 @@ class TestValidationService
|
|||||||
* @param string $resultType
|
* @param string $resultType
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function needsReferenceRanges(string $resultType): bool
|
public static function needsReferenceRanges(?string $resultType): bool
|
||||||
{
|
{
|
||||||
|
if ($resultType === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$resultType = strtoupper($resultType);
|
$resultType = strtoupper($resultType);
|
||||||
return $resultType !== 'NORES';
|
return $resultType !== 'NORES';
|
||||||
}
|
}
|
||||||
@ -195,7 +203,7 @@ class TestValidationService
|
|||||||
* @param string $refType
|
* @param string $refType
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function usesRefNum(string $resultType, string $refType): bool
|
public static function usesRefNum(?string $resultType, ?string $refType): bool
|
||||||
{
|
{
|
||||||
return self::getReferenceTable($resultType, $refType) === 'refnum';
|
return self::getReferenceTable($resultType, $refType) === 'refnum';
|
||||||
}
|
}
|
||||||
@ -207,7 +215,7 @@ class TestValidationService
|
|||||||
* @param string $refType
|
* @param string $refType
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function usesRefTxt(string $resultType, string $refType): bool
|
public static function usesRefTxt(?string $resultType, ?string $refType): bool
|
||||||
{
|
{
|
||||||
return self::getReferenceTable($resultType, $refType) === 'reftxt';
|
return self::getReferenceTable($resultType, $refType) === 'reftxt';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -214,20 +214,21 @@ class PatientModel extends BaseModel {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$InternalPID = $input['InternalPID'];
|
$InternalPID = $input['InternalPID'];
|
||||||
$previousData = $this->find($InternalPID);
|
$previousData = $this->find($InternalPID) ?? [];
|
||||||
$this->where('InternalPID',$InternalPID)->set($input)->update();
|
$this->where('InternalPID',$InternalPID)->set($input)->update();
|
||||||
$this->checkDbError($db, 'Update patient');
|
$this->checkDbError($db, 'Update patient');
|
||||||
|
|
||||||
|
$changedFields = array_keys(array_diff_assoc((array) $previousData, (array) $input));
|
||||||
AuditService::logData(
|
AuditService::logData(
|
||||||
'UPDATE',
|
'UPDATE',
|
||||||
'patient',
|
'patient',
|
||||||
(string) $InternalPID,
|
(string) $InternalPID,
|
||||||
'patient',
|
'patient',
|
||||||
null,
|
null,
|
||||||
$previousData,
|
(array) $previousData,
|
||||||
$input,
|
$input,
|
||||||
'Patient data updated',
|
'Patient data updated',
|
||||||
['changed_fields' => array_keys(array_diff_assoc($previousData, $input))]
|
['changed_fields' => $changedFields]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!empty($input['PatIdt'])) {
|
if (!empty($input['PatIdt'])) {
|
||||||
|
|||||||
@ -90,18 +90,19 @@ class RefTxtModel extends BaseModel
|
|||||||
*/
|
*/
|
||||||
public function batchInsert($testSiteID, $siteID, $ranges)
|
public function batchInsert($testSiteID, $siteID, $ranges)
|
||||||
{
|
{
|
||||||
foreach ($ranges as $range) {
|
foreach ($ranges as $range) {
|
||||||
$this->insert([
|
$this->insert([
|
||||||
'TestSiteID' => $testSiteID,
|
'TestSiteID' => $testSiteID,
|
||||||
'SiteID' => $siteID,
|
'SiteID' => $siteID,
|
||||||
'TxtRefType' => $range['TxtRefType'],
|
'SpcType' => $range['SpcType'] ?? 'GEN',
|
||||||
'Sex' => $range['Sex'],
|
'TxtRefType' => $range['TxtRefType'],
|
||||||
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
'Sex' => $range['Sex'],
|
||||||
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
|
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
||||||
'RefTxt' => $range['RefTxt'] ?? '',
|
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
|
||||||
'Flag' => $range['Flag'] ?? null,
|
'RefTxt' => $range['RefTxt'] ?? '',
|
||||||
'CreateDate' => date('Y-m-d H:i:s'),
|
'Flag' => $range['Flag'] ?? null,
|
||||||
]);
|
'CreateDate' => date('Y-m-d H:i:s'),
|
||||||
}
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,7 +74,7 @@ class RuleDefModel extends BaseModel
|
|||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
|
|
||||||
return array_column($result, 'TestSiteID');
|
return array_map('intval', array_column($result, 'TestSiteID'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,7 +93,8 @@ class RuleDefModel extends BaseModel
|
|||||||
->where('RuleID', $ruleID)
|
->where('RuleID', $ruleID)
|
||||||
->where('TestSiteID', $testSiteID)
|
->where('TestSiteID', $testSiteID)
|
||||||
->where('EndDate IS NULL')
|
->where('EndDate IS NULL')
|
||||||
->first();
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
return true; // Already linked
|
return true; // Already linked
|
||||||
@ -104,7 +105,8 @@ class RuleDefModel extends BaseModel
|
|||||||
->where('RuleID', $ruleID)
|
->where('RuleID', $ruleID)
|
||||||
->where('TestSiteID', $testSiteID)
|
->where('TestSiteID', $testSiteID)
|
||||||
->where('EndDate IS NOT NULL')
|
->where('EndDate IS NOT NULL')
|
||||||
->first();
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
if ($softDeleted) {
|
if ($softDeleted) {
|
||||||
return $db->table('testrule')
|
return $db->table('testrule')
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class AuditService {
|
|||||||
$this->db = \Config\Database::connect();
|
$this->db = \Config\Database::connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function logData(
|
public static function logData(
|
||||||
string $operation,
|
string $operation,
|
||||||
string $entityType,
|
string $entityType,
|
||||||
string $entityId,
|
string $entityId,
|
||||||
@ -28,8 +28,8 @@ class AuditService {
|
|||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'table_name' => $tableName,
|
'table_name' => $tableName,
|
||||||
'field_name' => $fieldName,
|
'field_name' => $fieldName,
|
||||||
'previous_value' => $previousValue,
|
'previous_value' => self::normalizeAuditValue($previousValue),
|
||||||
'new_value' => $newValue,
|
'new_value' => self::normalizeAuditValue($newValue),
|
||||||
'mechanism' => 'MANUAL',
|
'mechanism' => 'MANUAL',
|
||||||
'application_id' => 'CLQMS-WEB',
|
'application_id' => 'CLQMS-WEB',
|
||||||
'web_page' => self::getUri(),
|
'web_page' => self::getUri(),
|
||||||
@ -41,7 +41,7 @@ class AuditService {
|
|||||||
'ip_address' => self::getIpAddress(),
|
'ip_address' => self::getIpAddress(),
|
||||||
'user_id' => self::getUserId(),
|
'user_id' => self::getUserId(),
|
||||||
'reason' => $reason,
|
'reason' => $reason,
|
||||||
'context' => $context,
|
'context' => self::normalizeAuditValue($context),
|
||||||
'created_at' => date('Y-m-d H:i:s')
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -64,9 +64,9 @@ class AuditService {
|
|||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'service_class' => $serviceClass,
|
'service_class' => $serviceClass,
|
||||||
'resource_type' => $resourceType,
|
'resource_type' => $resourceType,
|
||||||
'resource_details' => $resourceDetails,
|
'resource_details' => self::normalizeAuditValue($resourceDetails),
|
||||||
'previous_value' => $previousValue,
|
'previous_value' => self::normalizeAuditValue($previousValue),
|
||||||
'new_value' => $newValue,
|
'new_value' => self::normalizeAuditValue($newValue),
|
||||||
'mechanism' => 'AUTOMATIC',
|
'mechanism' => 'AUTOMATIC',
|
||||||
'application_id' => $serviceName ?? 'SYSTEM-SERVICE',
|
'application_id' => $serviceName ?? 'SYSTEM-SERVICE',
|
||||||
'service_name' => $serviceName,
|
'service_name' => $serviceName,
|
||||||
@ -79,7 +79,7 @@ class AuditService {
|
|||||||
'port' => $resourceDetails['port'] ?? null,
|
'port' => $resourceDetails['port'] ?? null,
|
||||||
'user_id' => 'SYSTEM',
|
'user_id' => 'SYSTEM',
|
||||||
'reason' => null,
|
'reason' => null,
|
||||||
'context' => $context,
|
'context' => self::normalizeAuditValue($context),
|
||||||
'created_at' => date('Y-m-d H:i:s')
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -102,8 +102,8 @@ class AuditService {
|
|||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'security_class' => $securityClass,
|
'security_class' => $securityClass,
|
||||||
'resource_path' => $resourcePath,
|
'resource_path' => $resourcePath,
|
||||||
'previous_value' => $previousValue,
|
'previous_value' => self::normalizeAuditValue($previousValue),
|
||||||
'new_value' => $newValue,
|
'new_value' => self::normalizeAuditValue($newValue),
|
||||||
'mechanism' => 'MANUAL',
|
'mechanism' => 'MANUAL',
|
||||||
'application_id' => 'CLQMS-WEB',
|
'application_id' => 'CLQMS-WEB',
|
||||||
'web_page' => self::getUri(),
|
'web_page' => self::getUri(),
|
||||||
@ -115,7 +115,7 @@ class AuditService {
|
|||||||
'ip_address' => self::getIpAddress(),
|
'ip_address' => self::getIpAddress(),
|
||||||
'user_id' => self::getUserId() ?? 'UNKNOWN',
|
'user_id' => self::getUserId() ?? 'UNKNOWN',
|
||||||
'reason' => $reason,
|
'reason' => $reason,
|
||||||
'context' => $context,
|
'context' => self::normalizeAuditValue($context),
|
||||||
'created_at' => date('Y-m-d H:i:s')
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -138,9 +138,9 @@ class AuditService {
|
|||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'error_code' => $errorCode,
|
'error_code' => $errorCode,
|
||||||
'error_message' => $errorMessage,
|
'error_message' => $errorMessage,
|
||||||
'error_details' => $errorDetails,
|
'error_details' => self::normalizeAuditValue($errorDetails),
|
||||||
'previous_value' => $previousValue,
|
'previous_value' => self::normalizeAuditValue($previousValue),
|
||||||
'new_value' => $newValue,
|
'new_value' => self::normalizeAuditValue($newValue),
|
||||||
'mechanism' => 'AUTOMATIC',
|
'mechanism' => 'AUTOMATIC',
|
||||||
'application_id' => 'CLQMS-WEB',
|
'application_id' => 'CLQMS-WEB',
|
||||||
'web_page' => self::getUri(),
|
'web_page' => self::getUri(),
|
||||||
@ -152,16 +152,29 @@ class AuditService {
|
|||||||
'ip_address' => self::getIpAddress(),
|
'ip_address' => self::getIpAddress(),
|
||||||
'user_id' => self::getUserId() ?? 'SYSTEM',
|
'user_id' => self::getUserId() ?? 'SYSTEM',
|
||||||
'reason' => $reason,
|
'reason' => $reason,
|
||||||
'context' => $context,
|
'context' => self::normalizeAuditValue($context),
|
||||||
'created_at' => date('Y-m-d H:i:s')
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function log(string $table, array $data): void {
|
private static function log(string $table, array $data): void {
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
|
if (!$db->tableExists($table)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$db->table($table)->insert($data);
|
$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 {
|
private static function getUri(): ?string {
|
||||||
return $_SERVER['REQUEST_URI'] ?? null;
|
return $_SERVER['REQUEST_URI'] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ Rules are authored using a domain specific language stored in `ruledef.Condition
|
|||||||
### Execution Flow
|
### Execution Flow
|
||||||
|
|
||||||
1. Write or edit the DSL in `ConditionExpr`.
|
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`.
|
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).
|
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.
|
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)
|
└── 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
|
## 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`.
|
Validates the DSL and returns a compiled JSON structure that should be persisted in `ConditionExprCompiled`.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /api/rules/compile
|
POST /api/rule/compile
|
||||||
Content-Type: application/json
|
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.
|
This endpoint simply evaluates an expression against a runtime context. It does not compile DSL or persist the result.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /api/rules/validate
|
POST /api/rule/validate
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -179,7 +179,7 @@ Content-Type: application/json
|
|||||||
### Create Rule (example)
|
### Create Rule (example)
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /api/rules
|
POST /api/rule
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -211,7 +211,7 @@ Content-Type: application/json
|
|||||||
|
|
||||||
## Best Practices
|
## 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.
|
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.
|
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.
|
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`.
|
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`).
|
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).
|
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
|
### Invalid Expression
|
||||||
|
|
||||||
1. POST the expression to `/api/rules/compile` to get a detailed compilation error.
|
1. POST the expression to `/api/rule/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.
|
2. If using `/api/rule/validate`, supply the expected `context` payload; the endpoint simply evaluates the expression without saving it.
|
||||||
|
|
||||||
### Runtime Errors
|
### Runtime Errors
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<phpunit
|
<phpunit
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||||
bootstrap="vendor/codeigniter4/framework/system/Test/bootstrap.php"
|
bootstrap="tests/phpunit-bootstrap.php"
|
||||||
backupGlobals="false"
|
backupGlobals="false"
|
||||||
beStrictAboutOutputDuringTests="true"
|
beStrictAboutOutputDuringTests="true"
|
||||||
colors="true"
|
colors="true"
|
||||||
@ -43,7 +43,7 @@
|
|||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
<!-- Enable / Disable --> <!-- WAJIB DISESUAIKAN -->
|
<!-- Enable / Disable --> <!-- WAJIB DISESUAIKAN -->
|
||||||
<!-- <env name="CI_ENVIRONMENT" value="testing"/> -->
|
<env name="CI_ENVIRONMENT" value="testing"/>
|
||||||
|
|
||||||
<!-- <server name="app.baseURL" value="http://example.com/"/> -->
|
<!-- <server name="app.baseURL" value="http://example.com/"/> -->
|
||||||
<server name="app.baseURL" value="http://localhost/clqms01/"/> <!-- WAJIB DISESUAIKAN -->
|
<server name="app.baseURL" value="http://localhost/clqms01/"/> <!-- WAJIB DISESUAIKAN -->
|
||||||
@ -56,7 +56,7 @@
|
|||||||
<const name="PUBLICPATH" value="./public/"/>
|
<const name="PUBLICPATH" value="./public/"/>
|
||||||
<!-- Database configuration -->
|
<!-- Database configuration -->
|
||||||
<env name="database.tests.hostname" value="localhost"/> <!-- WAJIB DISESUAIKAN -->
|
<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.username" value="root"/> <!-- WAJIB DISESUAIKAN -->
|
||||||
<env name="database.tests.password" value="adminsakti"/> <!-- WAJIB DISESUAIKAN -->
|
<env name="database.tests.password" value="adminsakti"/> <!-- WAJIB DISESUAIKAN -->
|
||||||
<env name="database.tests.DBDriver" value="MySQLi"/> <!-- WAJIB DISESUAIKAN -->
|
<env name="database.tests.DBDriver" value="MySQLi"/> <!-- WAJIB DISESUAIKAN -->
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -25,40 +25,40 @@ servers:
|
|||||||
tags:
|
tags:
|
||||||
- name: Authentication
|
- name: Authentication
|
||||||
description: User authentication and session management
|
description: User authentication and session management
|
||||||
- name: Patients
|
- name: Patient
|
||||||
description: Patient registration and management
|
description: Patient registration and management
|
||||||
- name: Patient Visits
|
- name: Patient Visit
|
||||||
description: Patient visit/encounter management
|
description: Patient visit/encounter management
|
||||||
- name: Organization
|
- name: Organization
|
||||||
description: Organization structure (accounts, sites, disciplines, departments, workstations)
|
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
|
- name: Specimen
|
||||||
description: Specimen and container management
|
description: Specimen and container management
|
||||||
- name: Tests
|
- name: Test
|
||||||
description: Test definitions and test catalog
|
description: Test definitions and test catalog
|
||||||
- name: Calculations
|
- name: Rule
|
||||||
|
description: Rule engine - rules can be linked to multiple tests via testrule mapping table
|
||||||
|
- name: Calculation
|
||||||
description: Lightweight calculator endpoint for retrieving computed values by code or name
|
description: Lightweight calculator endpoint for retrieving computed values by code or name
|
||||||
- name: Orders
|
- name: Order
|
||||||
description: Laboratory order management
|
description: Laboratory order management
|
||||||
- name: Results
|
- name: Result
|
||||||
description: Patient results reporting with auto-validation
|
description: Patient results reporting with auto-validation
|
||||||
- name: Reports
|
- name: Report
|
||||||
description: Lab report generation (HTML view)
|
description: Lab report generation (HTML view)
|
||||||
- name: Edge API
|
- name: Edge API
|
||||||
description: Instrument integration endpoints
|
description: Instrument integration endpoints
|
||||||
- name: Contacts
|
- name: Contact
|
||||||
description: Contact management (doctors, practitioners, etc.)
|
description: Contact management (doctors, practitioners, etc.)
|
||||||
- name: Locations
|
- name: ValueSet
|
||||||
description: Location management (rooms, wards, buildings)
|
|
||||||
- name: ValueSets
|
|
||||||
description: Value set definitions and items
|
description: Value set definitions and items
|
||||||
|
- name: User
|
||||||
|
description: User management and administration
|
||||||
- name: Demo
|
- name: Demo
|
||||||
description: Demo/test endpoints (no authentication)
|
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
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/calc/{codeOrName}:
|
/api/calc/{codeOrName}:
|
||||||
post:
|
post:
|
||||||
tags: [Calculations]
|
tags: [Calculation]
|
||||||
summary: Evaluate a configured calculation by test code or name and return the numeric result only.
|
summary: Evaluate a configured calculation by test code or name and return the numeric result only.
|
||||||
security: []
|
security: []
|
||||||
parameters:
|
parameters:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/contact:
|
/api/contact:
|
||||||
get:
|
get:
|
||||||
tags: [Contacts]
|
tags: [Contact]
|
||||||
summary: List contacts
|
summary: List contacts
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -33,7 +33,7 @@
|
|||||||
$ref: '../components/schemas/master-data.yaml#/Contact'
|
$ref: '../components/schemas/master-data.yaml#/Contact'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Contacts]
|
tags: [Contact]
|
||||||
summary: Create new contact
|
summary: Create new contact
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -99,9 +99,10 @@
|
|||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||||
|
|
||||||
patch:
|
|
||||||
tags: [Contacts]
|
delete:
|
||||||
summary: Update contact
|
tags: [Contact]
|
||||||
|
summary: Delete contact
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -112,11 +113,67 @@
|
|||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- ContactID
|
- ContactID
|
||||||
- NameFirst
|
|
||||||
properties:
|
properties:
|
||||||
ContactID:
|
ContactID:
|
||||||
type: integer
|
type: integer
|
||||||
description: Contact ID to update
|
description: Contact ID to delete
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Contact deleted successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
||||||
|
|
||||||
|
/api/contact/{id}:
|
||||||
|
get:
|
||||||
|
tags: [Contact]
|
||||||
|
summary: Get contact by ID
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: Contact ID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Contact details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '../components/schemas/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:
|
NameFirst:
|
||||||
type: string
|
type: string
|
||||||
description: First name
|
description: First name
|
||||||
@ -169,56 +226,3 @@
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||||
|
|
||||||
delete:
|
|
||||||
tags: [Contacts]
|
|
||||||
summary: Delete contact
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- ContactID
|
|
||||||
properties:
|
|
||||||
ContactID:
|
|
||||||
type: integer
|
|
||||||
description: Contact ID to delete
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Contact deleted successfully
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
|
||||||
|
|
||||||
/api/contact/{id}:
|
|
||||||
get:
|
|
||||||
tags: [Contacts]
|
|
||||||
summary: Get contact by ID
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: Contact ID
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Contact details
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
data:
|
|
||||||
$ref: '../components/schemas/master-data.yaml#/Contact'
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/api/edge/results:
|
/api/edge/result:
|
||||||
post:
|
post:
|
||||||
tags: [Edge API]
|
tags: [Edge API]
|
||||||
summary: Receive results from instrument (tiny-edge)
|
summary: Receive results from instrument (tiny-edge)
|
||||||
@ -21,7 +21,7 @@
|
|||||||
'400':
|
'400':
|
||||||
description: Invalid JSON payload
|
description: Invalid JSON payload
|
||||||
|
|
||||||
/api/edge/orders:
|
/api/edge/order:
|
||||||
get:
|
get:
|
||||||
tags: [Edge API]
|
tags: [Edge API]
|
||||||
summary: Fetch pending orders for instruments
|
summary: Fetch pending orders for instruments
|
||||||
@ -53,7 +53,7 @@
|
|||||||
items:
|
items:
|
||||||
$ref: '../components/schemas/edge-api.yaml#/EdgeOrder'
|
$ref: '../components/schemas/edge-api.yaml#/EdgeOrder'
|
||||||
|
|
||||||
/api/edge/orders/{orderId}/ack:
|
/api/edge/order/{orderId}/ack:
|
||||||
post:
|
post:
|
||||||
tags: [Edge API]
|
tags: [Edge API]
|
||||||
summary: Acknowledge order delivery
|
summary: Acknowledge order delivery
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/equipmentlist:
|
/api/equipmentlist:
|
||||||
get:
|
get:
|
||||||
tags: [EquipmentList]
|
tags: [Equipment]
|
||||||
summary: List equipment
|
summary: List equipment
|
||||||
description: Get list of equipment with optional filters
|
description: Get list of equipment with optional filters
|
||||||
security:
|
security:
|
||||||
@ -50,7 +50,7 @@
|
|||||||
$ref: '../components/schemas/equipmentlist.yaml#/EquipmentList'
|
$ref: '../components/schemas/equipmentlist.yaml#/EquipmentList'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [EquipmentList]
|
tags: [Equipment]
|
||||||
summary: Create equipment
|
summary: Create equipment
|
||||||
description: Create a new equipment entry
|
description: Create a new equipment entry
|
||||||
security:
|
security:
|
||||||
@ -101,10 +101,11 @@
|
|||||||
data:
|
data:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
patch:
|
|
||||||
tags: [EquipmentList]
|
delete:
|
||||||
summary: Update equipment
|
tags: [Equipment]
|
||||||
description: Update an existing equipment entry
|
summary: Delete equipment
|
||||||
|
description: Soft delete an equipment entry
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -118,6 +119,68 @@
|
|||||||
properties:
|
properties:
|
||||||
EID:
|
EID:
|
||||||
type: integer
|
type: integer
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Equipment deleted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/api/equipmentlist/{id}:
|
||||||
|
get:
|
||||||
|
tags: [Equipment]
|
||||||
|
summary: Get equipment by ID
|
||||||
|
description: Get a single equipment entry by its EID
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: Equipment ID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Equipment details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '../components/schemas/equipmentlist.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:
|
IEID:
|
||||||
type: string
|
type: string
|
||||||
maxLength: 50
|
maxLength: 50
|
||||||
@ -151,62 +214,3 @@
|
|||||||
type: string
|
type: string
|
||||||
data:
|
data:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
delete:
|
|
||||||
tags: [EquipmentList]
|
|
||||||
summary: Delete equipment
|
|
||||||
description: Soft delete an equipment entry
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- EID
|
|
||||||
properties:
|
|
||||||
EID:
|
|
||||||
type: integer
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Equipment deleted
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
|
|
||||||
/api/equipmentlist/{id}:
|
|
||||||
get:
|
|
||||||
tags: [EquipmentList]
|
|
||||||
summary: Get equipment by ID
|
|
||||||
description: Get a single equipment entry by its EID
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: Equipment ID
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Equipment details
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
data:
|
|
||||||
$ref: '../components/schemas/equipmentlist.yaml#/EquipmentList'
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/location:
|
/api/location:
|
||||||
get:
|
get:
|
||||||
tags: [Locations]
|
tags: [Location]
|
||||||
summary: List locations
|
summary: List locations
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -33,7 +33,7 @@
|
|||||||
$ref: '../components/schemas/master-data.yaml#/Location'
|
$ref: '../components/schemas/master-data.yaml#/Location'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Locations]
|
tags: [Location]
|
||||||
summary: Create location
|
summary: Create location
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -83,9 +83,10 @@
|
|||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||||
|
|
||||||
patch:
|
|
||||||
tags: [Locations]
|
delete:
|
||||||
summary: Update location
|
tags: [Location]
|
||||||
|
summary: Delete location
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -99,7 +100,62 @@
|
|||||||
properties:
|
properties:
|
||||||
LocationID:
|
LocationID:
|
||||||
type: integer
|
type: integer
|
||||||
description: Location ID to update
|
description: Location ID to delete
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Location deleted successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
||||||
|
|
||||||
|
/api/location/{id}:
|
||||||
|
get:
|
||||||
|
tags: [Location]
|
||||||
|
summary: Get location by ID
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: Location ID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Location details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '../components/schemas/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:
|
SiteID:
|
||||||
type: integer
|
type: integer
|
||||||
description: Reference to site
|
description: Reference to site
|
||||||
@ -135,56 +191,3 @@
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||||
|
|
||||||
delete:
|
|
||||||
tags: [Locations]
|
|
||||||
summary: Delete location
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- LocationID
|
|
||||||
properties:
|
|
||||||
LocationID:
|
|
||||||
type: integer
|
|
||||||
description: Location ID to delete
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Location deleted successfully
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
|
||||||
|
|
||||||
/api/location/{id}:
|
|
||||||
get:
|
|
||||||
tags: [Locations]
|
|
||||||
summary: Get location by ID
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: Location ID
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Location details
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
data:
|
|
||||||
$ref: '../components/schemas/master-data.yaml#/Location'
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/ordertest:
|
/api/ordertest:
|
||||||
get:
|
get:
|
||||||
tags: [Orders]
|
tags: [Order]
|
||||||
summary: List orders
|
summary: List orders
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -48,7 +48,7 @@
|
|||||||
$ref: '../components/schemas/orders.yaml#/OrderTestList'
|
$ref: '../components/schemas/orders.yaml#/OrderTestList'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Orders]
|
tags: [Order]
|
||||||
summary: Create order with specimens and tests
|
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.
|
description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation.
|
||||||
security:
|
security:
|
||||||
@ -123,51 +123,9 @@
|
|||||||
'500':
|
'500':
|
||||||
description: Server error
|
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:
|
delete:
|
||||||
tags: [Orders]
|
tags: [Order]
|
||||||
summary: Delete order
|
summary: Delete order
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -188,7 +146,7 @@
|
|||||||
|
|
||||||
/api/ordertest/status:
|
/api/ordertest/status:
|
||||||
post:
|
post:
|
||||||
tags: [Orders]
|
tags: [Order]
|
||||||
summary: Update order status
|
summary: Update order status
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -231,7 +189,7 @@
|
|||||||
|
|
||||||
/api/ordertest/{id}:
|
/api/ordertest/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Orders]
|
tags: [Order]
|
||||||
summary: Get order by ID
|
summary: Get order by ID
|
||||||
description: Returns order details with associated specimens and tests
|
description: Returns order details with associated specimens and tests
|
||||||
security:
|
security:
|
||||||
@ -257,3 +215,49 @@
|
|||||||
type: string
|
type: string
|
||||||
data:
|
data:
|
||||||
$ref: '../components/schemas/orders.yaml#/OrderTest'
|
$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':
|
'201':
|
||||||
description: Site created
|
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:
|
delete:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -105,6 +80,34 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Site details
|
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:
|
/api/organization/discipline:
|
||||||
get:
|
get:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -130,35 +133,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Discipline created
|
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:
|
delete:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -196,6 +170,38 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Discipline details
|
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:
|
/api/organization/department:
|
||||||
get:
|
get:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -221,31 +227,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Department created
|
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:
|
delete:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -283,6 +264,34 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Department details
|
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:
|
/api/organization/workstation:
|
||||||
get:
|
get:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -308,33 +317,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Workstation created
|
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:
|
delete:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -372,6 +354,36 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Workstation details
|
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
|
# HostApp
|
||||||
/api/organization/hostapp:
|
/api/organization/hostapp:
|
||||||
get:
|
get:
|
||||||
@ -420,29 +432,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Host application created
|
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:
|
delete:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -484,6 +473,32 @@
|
|||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/organization.yaml#/HostApp'
|
$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
|
# HostComPara
|
||||||
/api/organization/hostcompara:
|
/api/organization/hostcompara:
|
||||||
get:
|
get:
|
||||||
@ -532,31 +547,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Host communication parameters created
|
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:
|
delete:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -598,6 +588,34 @@
|
|||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/organization.yaml#/HostComPara'
|
$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
|
# CodingSys
|
||||||
/api/organization/codingsys:
|
/api/organization/codingsys:
|
||||||
get:
|
get:
|
||||||
@ -646,31 +664,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Coding system created
|
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:
|
delete:
|
||||||
tags: [Organization]
|
tags: [Organization]
|
||||||
@ -711,3 +704,31 @@
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/organization.yaml#/CodingSys'
|
$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:
|
/api/patvisit:
|
||||||
get:
|
get:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: List patient visits
|
summary: List patient visits
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -72,7 +72,7 @@
|
|||||||
description: Number of records per page
|
description: Number of records per page
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: Create patient visit
|
summary: Create patient visit
|
||||||
description: |
|
description: |
|
||||||
Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided.
|
Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided.
|
||||||
@ -145,26 +145,66 @@
|
|||||||
InternalPVID:
|
InternalPVID:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags: [Patient Visit]
|
||||||
|
summary: Delete patient visit
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Visit deleted successfully
|
||||||
|
|
||||||
|
/api/patvisit/{id}:
|
||||||
|
get:
|
||||||
|
tags: [Patient Visit]
|
||||||
|
summary: Get visit by ID
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: PVID (visit identifier like DV00001)
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Visit details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '../components/schemas/patient-visit.yaml#/PatientVisit'
|
||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: Update patient visit
|
summary: Update patient visit
|
||||||
description: |
|
description: |
|
||||||
Updates an existing patient visit. InternalPVID is required.
|
Updates an existing patient visit. InternalPVID is required.
|
||||||
Can update main visit data, PatDiag, and add new PatVisitADT records.
|
Can update main visit data, PatDiag, and add new PatVisitADT records.
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: Internal visit ID (InternalPVID)
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- InternalPVID
|
|
||||||
properties:
|
properties:
|
||||||
InternalPVID:
|
|
||||||
type: integer
|
|
||||||
description: Visit ID (required)
|
|
||||||
PVID:
|
PVID:
|
||||||
type: string
|
type: string
|
||||||
InternalPID:
|
InternalPID:
|
||||||
@ -223,46 +263,9 @@
|
|||||||
InternalPVID:
|
InternalPVID:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
delete:
|
|
||||||
tags: [Patient Visits]
|
|
||||||
summary: Delete patient visit
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Visit deleted successfully
|
|
||||||
|
|
||||||
/api/patvisit/{id}:
|
|
||||||
get:
|
|
||||||
tags: [Patient Visits]
|
|
||||||
summary: Get visit by ID
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: PVID (visit identifier like DV00001)
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Visit details
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
data:
|
|
||||||
$ref: '../components/schemas/patient-visit.yaml#/PatientVisit'
|
|
||||||
|
|
||||||
/api/patvisit/patient/{patientId}:
|
/api/patvisit/patient/{patientId}:
|
||||||
get:
|
get:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: Get visits by patient ID
|
summary: Get visits by patient ID
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -290,7 +293,7 @@
|
|||||||
|
|
||||||
/api/patvisitadt:
|
/api/patvisitadt:
|
||||||
post:
|
post:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: Create ADT record
|
summary: Create ADT record
|
||||||
description: Create a new Admission/Discharge/Transfer record
|
description: Create a new Admission/Discharge/Transfer record
|
||||||
security:
|
security:
|
||||||
@ -309,28 +312,10 @@
|
|||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
$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:
|
delete:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: Delete ADT visit (soft delete)
|
summary: Delete ADT visit (soft delete)
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -352,7 +337,7 @@
|
|||||||
|
|
||||||
/api/patvisitadt/visit/{visitId}:
|
/api/patvisitadt/visit/{visitId}:
|
||||||
get:
|
get:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: Get ADT history by visit ID
|
summary: Get ADT history by visit ID
|
||||||
description: Retrieve the complete Admission/Discharge/Transfer history for a visit, including all locations and doctors
|
description: Retrieve the complete Admission/Discharge/Transfer history for a visit, including all locations and doctors
|
||||||
security:
|
security:
|
||||||
@ -424,30 +409,9 @@
|
|||||||
EndDate:
|
EndDate:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
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}:
|
/api/patvisitadt/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visit]
|
||||||
summary: Get ADT record by ID
|
summary: Get ADT record by ID
|
||||||
description: Retrieve a single ADT record by its ID, including location and doctor details
|
description: Retrieve a single ADT record by its ID, including location and doctor details
|
||||||
security:
|
security:
|
||||||
@ -517,3 +481,30 @@
|
|||||||
EndDate:
|
EndDate:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
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:
|
/api/patient:
|
||||||
get:
|
get:
|
||||||
tags: [Patients]
|
tags: [Patient]
|
||||||
summary: List patients
|
summary: List patients
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -45,7 +45,7 @@
|
|||||||
$ref: '../components/schemas/patient.yaml#/PatientListResponse'
|
$ref: '../components/schemas/patient.yaml#/PatientListResponse'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Patients]
|
tags: [Patient]
|
||||||
summary: Create new patient
|
summary: Create new patient
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -69,23 +69,9 @@
|
|||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
$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:
|
delete:
|
||||||
tags: [Patients]
|
tags: [Patient]
|
||||||
summary: Delete patient (soft delete)
|
summary: Delete patient (soft delete)
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -107,7 +93,7 @@
|
|||||||
|
|
||||||
/api/patient/check:
|
/api/patient/check:
|
||||||
get:
|
get:
|
||||||
tags: [Patients]
|
tags: [Patient]
|
||||||
summary: Check if patient exists
|
summary: Check if patient exists
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -138,7 +124,7 @@
|
|||||||
|
|
||||||
/api/patient/{id}:
|
/api/patient/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Patients]
|
tags: [Patient]
|
||||||
summary: Get patient by ID
|
summary: Get patient by ID
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -161,3 +147,25 @@
|
|||||||
type: string
|
type: string
|
||||||
data:
|
data:
|
||||||
$ref: '../components/schemas/patient.yaml#/Patient'
|
$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}:
|
/api/report/{orderID}:
|
||||||
get:
|
get:
|
||||||
tags: [Reports]
|
tags: [Report]
|
||||||
summary: Generate lab 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.
|
description: Generate an HTML lab report for a specific order. Returns HTML content that can be viewed in browser or printed to PDF.
|
||||||
security:
|
security:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/results:
|
/api/result:
|
||||||
get:
|
get:
|
||||||
tags: [Results]
|
tags: [Result]
|
||||||
summary: List results
|
summary: List results
|
||||||
description: Retrieve patient test results with optional filters by order or patient
|
description: Retrieve patient test results with optional filters by order or patient
|
||||||
security:
|
security:
|
||||||
@ -94,9 +94,9 @@
|
|||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
||||||
/api/results/{id}:
|
/api/result/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Results]
|
tags: [Result]
|
||||||
summary: Get result by ID
|
summary: Get result by ID
|
||||||
description: Retrieve a specific result entry with all related data
|
description: Retrieve a specific result entry with all related data
|
||||||
security:
|
security:
|
||||||
@ -203,7 +203,7 @@
|
|||||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags: [Results]
|
tags: [Result]
|
||||||
summary: Update 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.
|
description: Update a result value with automatic validation against reference ranges. Returns calculated flag (L/H) in response but does not store it.
|
||||||
security:
|
security:
|
||||||
@ -274,7 +274,7 @@
|
|||||||
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [Results]
|
tags: [Result]
|
||||||
summary: Delete result
|
summary: Delete result
|
||||||
description: Soft delete a result entry by setting DelDate
|
description: Soft delete a result entry by setting DelDate
|
||||||
security:
|
security:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/rules:
|
/api/rule:
|
||||||
get:
|
get:
|
||||||
tags: [Rules]
|
tags: [Rule]
|
||||||
summary: List rules
|
summary: List rules
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -38,7 +38,7 @@
|
|||||||
$ref: '../components/schemas/rules.yaml#/RuleDef'
|
$ref: '../components/schemas/rules.yaml#/RuleDef'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Rules]
|
tags: [Rule]
|
||||||
summary: Create rule
|
summary: Create rule
|
||||||
description: |
|
description: |
|
||||||
Create a new rule. Rules must be linked to at least one test via TestSiteIDs.
|
Create a new rule. Rules must be linked to at least one test via TestSiteIDs.
|
||||||
@ -77,15 +77,15 @@
|
|||||||
ConditionExprCompiled:
|
ConditionExprCompiled:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
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]
|
required: [RuleCode, RuleName, EventCode, TestSiteIDs]
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: Rule created
|
description: Rule created
|
||||||
|
|
||||||
/api/rules/{id}:
|
/api/rule/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Rules]
|
tags: [Rule]
|
||||||
summary: Get rule with linked tests
|
summary: Get rule with linked tests
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -114,7 +114,7 @@
|
|||||||
description: Rule not found
|
description: Rule not found
|
||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags: [Rules]
|
tags: [Rule]
|
||||||
summary: Update rule
|
summary: Update rule
|
||||||
description: |
|
description: |
|
||||||
Update a rule. TestSiteIDs can be provided to update which tests the rule is linked to.
|
Update a rule. TestSiteIDs can be provided to update which tests the rule is linked to.
|
||||||
@ -153,7 +153,7 @@
|
|||||||
description: Rule not found
|
description: Rule not found
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [Rules]
|
tags: [Rule]
|
||||||
summary: Soft delete rule
|
summary: Soft delete rule
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -170,9 +170,9 @@
|
|||||||
'404':
|
'404':
|
||||||
description: Rule not found
|
description: Rule not found
|
||||||
|
|
||||||
/api/rules/validate:
|
/api/rule/validate:
|
||||||
post:
|
post:
|
||||||
tags: [Rules]
|
tags: [Rule]
|
||||||
summary: Validate/evaluate an expression
|
summary: Validate/evaluate an expression
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -193,9 +193,9 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Validation result
|
description: Validation result
|
||||||
|
|
||||||
/api/rules/compile:
|
/api/rule/compile:
|
||||||
post:
|
post:
|
||||||
tags: [Rules]
|
tags: [Rule]
|
||||||
summary: Compile DSL expression to engine-compatible structure
|
summary: Compile DSL expression to engine-compatible structure
|
||||||
description: |
|
description: |
|
||||||
Compile a DSL expression to the engine-compatible JSON structure.
|
Compile a DSL expression to the engine-compatible JSON structure.
|
||||||
|
|||||||
@ -23,20 +23,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Specimen created
|
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}:
|
/api/specimen/{id}:
|
||||||
get:
|
get:
|
||||||
@ -54,6 +40,28 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Specimen details
|
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:
|
delete:
|
||||||
tags: [Specimen]
|
tags: [Specimen]
|
||||||
summary: Delete specimen (soft delete)
|
summary: Delete specimen (soft delete)
|
||||||
@ -141,14 +149,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Container definition created
|
description: Container definition created
|
||||||
|
|
||||||
patch:
|
|
||||||
tags: [Specimen]
|
|
||||||
summary: Update container definition
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Container definition updated
|
|
||||||
|
|
||||||
/api/specimen/container/{id}:
|
/api/specimen/container/{id}:
|
||||||
get:
|
get:
|
||||||
@ -166,6 +166,28 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Container definition details
|
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:
|
/api/specimen/containerdef:
|
||||||
get:
|
get:
|
||||||
tags: [Specimen]
|
tags: [Specimen]
|
||||||
@ -191,11 +213,26 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Container definition created
|
description: Container definition created
|
||||||
|
|
||||||
|
|
||||||
|
/api/specimen/containerdef/{id}:
|
||||||
patch:
|
patch:
|
||||||
tags: [Specimen]
|
tags: [Specimen]
|
||||||
summary: Update container definition (alias)
|
summary: Update container definition (alias)
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- 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:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Container definition updated
|
description: Container definition updated
|
||||||
@ -225,14 +262,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Specimen preparation created
|
description: Specimen preparation created
|
||||||
|
|
||||||
patch:
|
|
||||||
tags: [Specimen]
|
|
||||||
summary: Update specimen preparation
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Specimen preparation updated
|
|
||||||
|
|
||||||
/api/specimen/prep/{id}:
|
/api/specimen/prep/{id}:
|
||||||
get:
|
get:
|
||||||
@ -250,6 +279,28 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Specimen preparation details
|
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:
|
/api/specimen/status:
|
||||||
get:
|
get:
|
||||||
tags: [Specimen]
|
tags: [Specimen]
|
||||||
@ -275,14 +326,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Specimen status created
|
description: Specimen status created
|
||||||
|
|
||||||
patch:
|
|
||||||
tags: [Specimen]
|
|
||||||
summary: Update specimen status
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Specimen status updated
|
|
||||||
|
|
||||||
/api/specimen/status/{id}:
|
/api/specimen/status/{id}:
|
||||||
get:
|
get:
|
||||||
@ -300,6 +343,28 @@
|
|||||||
'200':
|
'200':
|
||||||
description: Specimen status details
|
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:
|
/api/specimen/collection:
|
||||||
get:
|
get:
|
||||||
tags: [Specimen]
|
tags: [Specimen]
|
||||||
@ -325,14 +390,6 @@
|
|||||||
'201':
|
'201':
|
||||||
description: Collection method created
|
description: Collection method created
|
||||||
|
|
||||||
patch:
|
|
||||||
tags: [Specimen]
|
|
||||||
summary: Update specimen collection method
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Collection method updated
|
|
||||||
|
|
||||||
/api/specimen/collection/{id}:
|
/api/specimen/collection/{id}:
|
||||||
get:
|
get:
|
||||||
@ -349,3 +406,25 @@
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Collection method details
|
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:
|
/api/test/testmap:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: List all test mappings
|
summary: List all test mappings
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -38,7 +38,7 @@
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Create test mapping (header only)
|
summary: Create test mapping (header only)
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -99,53 +99,9 @@
|
|||||||
type: integer
|
type: integer
|
||||||
description: Created TestMapID
|
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:
|
delete:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Soft delete test mapping (cascades to details)
|
summary: Soft delete test mapping (cascades to details)
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -182,7 +138,7 @@
|
|||||||
|
|
||||||
/api/test/testmap/{id}:
|
/api/test/testmap/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Get test mapping by ID with details
|
summary: Get test mapping by ID with details
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -210,9 +166,56 @@
|
|||||||
'404':
|
'404':
|
||||||
description: Test mapping not found
|
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}:
|
/api/test/testmap/by-testcode/{testCode}:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Get test mappings by test code with details
|
summary: Get test mappings by test code with details
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -242,7 +245,7 @@
|
|||||||
|
|
||||||
/api/test/testmap/detail:
|
/api/test/testmap/detail:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: List test mapping details
|
summary: List test mapping details
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -270,7 +273,7 @@
|
|||||||
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
|
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Create test mapping detail
|
summary: Create test mapping detail
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -312,41 +315,9 @@
|
|||||||
type: integer
|
type: integer
|
||||||
description: Created TestMapDetailID
|
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:
|
delete:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Soft delete test mapping detail
|
summary: Soft delete test mapping detail
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -368,7 +339,7 @@
|
|||||||
|
|
||||||
/api/test/testmap/detail/{id}:
|
/api/test/testmap/detail/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Get test mapping detail by ID
|
summary: Get test mapping detail by ID
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -394,9 +365,44 @@
|
|||||||
data:
|
data:
|
||||||
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
|
$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}:
|
/api/test/testmap/detail/by-testmap/{testMapID}:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Get test mapping details by test map ID
|
summary: Get test mapping details by test map ID
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -426,7 +432,7 @@
|
|||||||
|
|
||||||
/api/test/testmap/detail/batch:
|
/api/test/testmap/detail/batch:
|
||||||
post:
|
post:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Batch create test mapping details
|
summary: Batch create test mapping details
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -456,7 +462,7 @@
|
|||||||
description: Batch create results
|
description: Batch create results
|
||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Batch update test mapping details
|
summary: Batch update test mapping details
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -488,7 +494,7 @@
|
|||||||
description: Batch update results
|
description: Batch update results
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Batch delete test mapping details
|
summary: Batch delete test mapping details
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/test:
|
/api/test:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: List test definitions
|
summary: List test definitions
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -67,7 +67,7 @@
|
|||||||
description: Total number of records matching the query
|
description: Total number of records matching the query
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Create test definition
|
summary: Create test definition
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -205,29 +205,462 @@
|
|||||||
- TestSiteName
|
- TestSiteName
|
||||||
- TestType
|
- TestType
|
||||||
examples:
|
examples:
|
||||||
CALC_test:
|
TEST_no_ref:
|
||||||
summary: Create calculated test with members
|
summary: Technical test without reference or map
|
||||||
value:
|
value:
|
||||||
SiteID: 1
|
SiteID: 1
|
||||||
TestSiteCode: IBIL
|
TestSiteCode: TEST_NREF
|
||||||
TestSiteName: Indirect Bilirubin
|
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
|
TestType: CALC
|
||||||
Description: Bilirubin Indirek
|
SeqScr: 190
|
||||||
SeqScr: 210
|
SeqRpt: 190
|
||||||
SeqRpt: 210
|
|
||||||
VisibleScr: 1
|
VisibleScr: 1
|
||||||
VisibleRpt: 1
|
VisibleRpt: 1
|
||||||
CountStat: 0
|
CountStat: 0
|
||||||
details:
|
details:
|
||||||
DisciplineID: 2
|
DisciplineID: 2
|
||||||
DepartmentID: 2
|
DepartmentID: 2
|
||||||
FormulaCode: "{TBIL} - {DBIL}"
|
FormulaCode: CKD_EPI(CREA,AGE,GENDER)
|
||||||
RefType: RANGE
|
|
||||||
Unit1: mg/dL
|
|
||||||
Decimal: 2
|
|
||||||
members:
|
members:
|
||||||
|
- TestSiteID: 21
|
||||||
- TestSiteID: 22
|
- 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:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: Test definition created
|
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.'
|
example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.'
|
||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Update test definition
|
summary: Update test definition
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -426,7 +859,7 @@
|
|||||||
|
|
||||||
/api/test/{id}:
|
/api/test/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Get test definition by ID
|
summary: Get test definition by ID
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -455,7 +888,7 @@
|
|||||||
description: Test not found
|
description: Test not found
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [Tests]
|
tags: [Test]
|
||||||
summary: Soft delete test definition
|
summary: Soft delete test definition
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/users:
|
/api/user:
|
||||||
get:
|
get:
|
||||||
tags: [Users]
|
tags: [User]
|
||||||
summary: List users with pagination and search
|
summary: List users with pagination and search
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -58,7 +58,7 @@
|
|||||||
description: Server error
|
description: Server error
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [Users]
|
tags: [User]
|
||||||
summary: Create new user
|
summary: Create new user
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
@ -109,11 +109,44 @@
|
|||||||
'500':
|
'500':
|
||||||
description: Server error
|
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:
|
patch:
|
||||||
tags: [Users]
|
tags: [User]
|
||||||
summary: Update existing user
|
summary: Update existing user
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: User ID
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -150,33 +183,8 @@
|
|||||||
'500':
|
'500':
|
||||||
description: Server error
|
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:
|
delete:
|
||||||
tags: [Users]
|
tags: [User]
|
||||||
summary: Delete user (soft delete)
|
summary: Delete user (soft delete)
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/api/valueset:
|
/api/valueset:
|
||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: List lib value sets
|
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.
|
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:
|
security:
|
||||||
@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
/api/valueset/{key}:
|
/api/valueset/{key}:
|
||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Get lib value set by key
|
summary: Get lib value set by key
|
||||||
description: |
|
description: |
|
||||||
Get a specific library/system value set from JSON files.
|
Get a specific library/system value set from JSON files.
|
||||||
@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
/api/valueset/refresh:
|
/api/valueset/refresh:
|
||||||
post:
|
post:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Refresh lib ValueSet cache
|
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/.
|
description: Clear and reload the library/system ValueSet cache from JSON files. Call this after modifying JSON files in app/Libraries/Data/.
|
||||||
security:
|
security:
|
||||||
@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
/api/valueset/user/items:
|
/api/valueset/user/items:
|
||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: List user value set items
|
summary: List user value set items
|
||||||
description: List value set items from database (user-defined)
|
description: List value set items from database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -178,7 +178,7 @@
|
|||||||
$ref: '../components/schemas/valuesets.yaml#/ValueSetItem'
|
$ref: '../components/schemas/valuesets.yaml#/ValueSetItem'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Create user value set item
|
summary: Create user value set item
|
||||||
description: Create value set item in database (user-defined)
|
description: Create value set item in database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -224,7 +224,7 @@
|
|||||||
|
|
||||||
/api/valueset/user/items/{id}:
|
/api/valueset/user/items/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Get user value set item by ID
|
summary: Get user value set item by ID
|
||||||
description: Get value set item from database (user-defined)
|
description: Get value set item from database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -249,7 +249,7 @@
|
|||||||
$ref: '../components/schemas/valuesets.yaml#/ValueSetItem'
|
$ref: '../components/schemas/valuesets.yaml#/ValueSetItem'
|
||||||
|
|
||||||
put:
|
put:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Update user value set item
|
summary: Update user value set item
|
||||||
description: Update value set item in database (user-defined)
|
description: Update value set item in database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -298,7 +298,7 @@
|
|||||||
$ref: '../components/schemas/valuesets.yaml#/ValueSetItem'
|
$ref: '../components/schemas/valuesets.yaml#/ValueSetItem'
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Delete user value set item
|
summary: Delete user value set item
|
||||||
description: Delete value set item from database (user-defined)
|
description: Delete value set item from database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -324,7 +324,7 @@
|
|||||||
|
|
||||||
/api/valueset/user/def:
|
/api/valueset/user/def:
|
||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: List user value set definitions
|
summary: List user value set definitions
|
||||||
description: List value set definitions from database (user-defined)
|
description: List value set definitions from database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -372,7 +372,7 @@
|
|||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Create user value set definition
|
summary: Create user value set definition
|
||||||
description: Create value set definition in database (user-defined)
|
description: Create value set definition in database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -410,7 +410,7 @@
|
|||||||
|
|
||||||
/api/valueset/user/def/{id}:
|
/api/valueset/user/def/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Get user value set definition by ID
|
summary: Get user value set definition by ID
|
||||||
description: Get value set definition from database (user-defined)
|
description: Get value set definition from database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -435,7 +435,7 @@
|
|||||||
$ref: '../components/schemas/valuesets.yaml#/ValueSetDef'
|
$ref: '../components/schemas/valuesets.yaml#/ValueSetDef'
|
||||||
|
|
||||||
put:
|
put:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Update user value set definition
|
summary: Update user value set definition
|
||||||
description: Update value set definition in database (user-defined)
|
description: Update value set definition in database (user-defined)
|
||||||
security:
|
security:
|
||||||
@ -478,7 +478,7 @@
|
|||||||
$ref: '../components/schemas/valuesets.yaml#/ValueSetDef'
|
$ref: '../components/schemas/valuesets.yaml#/ValueSetDef'
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [ValueSets]
|
tags: [ValueSet]
|
||||||
summary: Delete user value set definition
|
summary: Delete user value set definition
|
||||||
description: Delete value set definition from database (user-defined)
|
description: Delete value set definition from database (user-defined)
|
||||||
security:
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
public function testCreateCodingSys()
|
||||||
{
|
{
|
||||||
$payload = [
|
$payload = [
|
||||||
'CodingSysAbb' => 'ICD10',
|
'CodingSysAbb' => 'ICD' . substr(time(), -3),
|
||||||
'FullText' => 'International Classification of Diseases 10',
|
'FullText' => 'International Classification of Diseases 10 ' . time(),
|
||||||
'Description' => 'Medical diagnosis coding system'
|
'Description' => 'Medical diagnosis coding system'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class HostAppControllerTest extends CIUnitTestCase
|
|||||||
|
|
||||||
protected $endpoint = 'api/organization/hostapp';
|
protected $endpoint = 'api/organization/hostapp';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
public function testIndexHostApp()
|
public function testIndexHostApp()
|
||||||
{
|
{
|
||||||
$result = $this->get($this->endpoint);
|
$result = $this->get($this->endpoint);
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class PatVisitByPatientTest extends CIUnitTestCase
|
|||||||
|
|
||||||
protected $endpoint = 'api/patvisit/patient';
|
protected $endpoint = 'api/patvisit/patient';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test: Show all visits by valid InternalPID
|
* Test: Show all visits by valid InternalPID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,20 +4,27 @@ namespace Tests\Feature\PatVisit;
|
|||||||
|
|
||||||
use CodeIgniter\Test\FeatureTestTrait;
|
use CodeIgniter\Test\FeatureTestTrait;
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use Tests\Support\Traits\CreatesPatients;
|
||||||
|
|
||||||
class PatVisitCreateTest extends CIUnitTestCase
|
class PatVisitCreateTest extends CIUnitTestCase
|
||||||
{
|
{
|
||||||
use FeatureTestTrait;
|
use FeatureTestTrait;
|
||||||
|
use CreatesPatients;
|
||||||
|
|
||||||
protected $endpoint = 'api/patvisit';
|
protected $endpoint = 'api/patvisit';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test: Create patient visit with valid data
|
* Test: Create patient visit with valid data
|
||||||
*/
|
*/
|
||||||
public function testCreatePatientVisitSuccess()
|
public function testCreatePatientVisitSuccess()
|
||||||
{
|
{
|
||||||
$payload = [
|
$payload = [
|
||||||
"InternalPID"=> "1",
|
"InternalPID"=> $this->createTestPatient(),
|
||||||
"EpisodeID"=> null,
|
"EpisodeID"=> null,
|
||||||
"PatDiag"=> [
|
"PatDiag"=> [
|
||||||
"DiagCode"=> null,
|
"DiagCode"=> null,
|
||||||
|
|||||||
@ -4,13 +4,20 @@ namespace Tests\Feature\PatVisit;
|
|||||||
|
|
||||||
use CodeIgniter\Test\FeatureTestTrait;
|
use CodeIgniter\Test\FeatureTestTrait;
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use Tests\Support\Traits\CreatesPatients;
|
||||||
|
|
||||||
class PatVisitDeleteTest extends CIUnitTestCase
|
class PatVisitDeleteTest extends CIUnitTestCase
|
||||||
{
|
{
|
||||||
use FeatureTestTrait;
|
use FeatureTestTrait;
|
||||||
|
use CreatesPatients;
|
||||||
|
|
||||||
protected $endpoint = 'api/patvisit';
|
protected $endpoint = 'api/patvisit';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test: Delete patient visit successfully (soft delete)
|
* Test: Delete patient visit successfully (soft delete)
|
||||||
*/
|
*/
|
||||||
@ -18,7 +25,7 @@ class PatVisitDeleteTest extends CIUnitTestCase
|
|||||||
{
|
{
|
||||||
// Create a visit first to delete
|
// Create a visit first to delete
|
||||||
$createPayload = [
|
$createPayload = [
|
||||||
"InternalPID"=> "1",
|
"InternalPID"=> $this->createTestPatient(),
|
||||||
"EpisodeID"=> "TEST001",
|
"EpisodeID"=> "TEST001",
|
||||||
"PatVisitADT"=> [
|
"PatVisitADT"=> [
|
||||||
"ADTCode"=> "A01",
|
"ADTCode"=> "A01",
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class PatVisitShowTest extends CIUnitTestCase
|
|||||||
|
|
||||||
protected $endpoint = 'api/patvisit';
|
protected $endpoint = 'api/patvisit';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
public function testShowPatientVisitSuccess()
|
public function testShowPatientVisitSuccess()
|
||||||
{
|
{
|
||||||
$PVID = '1';
|
$PVID = '1';
|
||||||
|
|||||||
@ -4,12 +4,19 @@ namespace Tests\Feature\PatVisit;
|
|||||||
|
|
||||||
use CodeIgniter\Test\FeatureTestTrait;
|
use CodeIgniter\Test\FeatureTestTrait;
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use Tests\Support\Traits\CreatesPatients;
|
||||||
|
|
||||||
class PatVisitUpdateTest extends CIUnitTestCase
|
class PatVisitUpdateTest extends CIUnitTestCase
|
||||||
{
|
{
|
||||||
use FeatureTestTrait;
|
use FeatureTestTrait;
|
||||||
|
use CreatesPatients;
|
||||||
protected $endpoint = 'api/patvisit';
|
protected $endpoint = 'api/patvisit';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test: Update patient visit successfully
|
* Test: Update patient visit successfully
|
||||||
*/
|
*/
|
||||||
@ -17,7 +24,7 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
{
|
{
|
||||||
// First create a visit to update
|
// First create a visit to update
|
||||||
$createPayload = [
|
$createPayload = [
|
||||||
"InternalPID"=> "1",
|
"InternalPID"=> $this->createTestPatient(),
|
||||||
"EpisodeID"=> "TEST001",
|
"EpisodeID"=> "TEST001",
|
||||||
"PatVisitADT"=> [
|
"PatVisitADT"=> [
|
||||||
"ADTCode"=> "A01",
|
"ADTCode"=> "A01",
|
||||||
@ -34,7 +41,6 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
|
|
||||||
// Now update it
|
// Now update it
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPVID' => $internalPVID,
|
|
||||||
'PVID' => $pvid,
|
'PVID' => $pvid,
|
||||||
'EpisodeID' => 'EPI001',
|
'EpisodeID' => 'EPI001',
|
||||||
'PatDiag' => [
|
'PatDiag' => [
|
||||||
@ -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)
|
// Pastikan response sukses (200 OK untuk update)
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
@ -75,8 +81,8 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
'PatDiag' => ['DiagCode' => 'B01', 'DiagName' => 'Flu']
|
'PatDiag' => ['DiagCode' => 'B01', 'DiagName' => 'Flu']
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload);
|
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/0', $payload);
|
||||||
// Karena ID tidak ada → 400 Bad Request
|
// Karena ID tidak valid → 400 Bad Request
|
||||||
$response->assertStatus(400);
|
$response->assertStatus(400);
|
||||||
|
|
||||||
$response->assertJSONFragment([
|
$response->assertJSONFragment([
|
||||||
@ -91,7 +97,6 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
public function testUpdatePatientVisitNotFound()
|
public function testUpdatePatientVisitNotFound()
|
||||||
{
|
{
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPVID' => 999999, // Non-existent visit
|
|
||||||
'EpisodeID' => 'EPI001',
|
'EpisodeID' => 'EPI001',
|
||||||
'PatDiag' => [
|
'PatDiag' => [
|
||||||
'DiagCode' => 'A02',
|
'DiagCode' => 'A02',
|
||||||
@ -99,7 +104,7 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload);
|
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/999999', $payload);
|
||||||
$response->assertStatus(404);
|
$response->assertStatus(404);
|
||||||
$response->assertJSONFragment([
|
$response->assertJSONFragment([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
@ -113,7 +118,6 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
public function testUpdatePatientVisitInvalidId()
|
public function testUpdatePatientVisitInvalidId()
|
||||||
{
|
{
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPVID' => 'invalid',
|
|
||||||
'PVID' => 'DV0001',
|
'PVID' => 'DV0001',
|
||||||
'EpisodeID' => 'EPI001',
|
'EpisodeID' => 'EPI001',
|
||||||
'PatDiag' => [
|
'PatDiag' => [
|
||||||
@ -126,7 +130,7 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload);
|
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/invalid', $payload);
|
||||||
$response->assertStatus(400);
|
$response->assertStatus(400);
|
||||||
$response->assertJSONFragment([
|
$response->assertJSONFragment([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
@ -141,7 +145,7 @@ class PatVisitUpdateTest extends CIUnitTestCase
|
|||||||
{
|
{
|
||||||
$payload = [];
|
$payload = [];
|
||||||
|
|
||||||
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload);
|
$response = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/0', $payload);
|
||||||
$response->assertStatus(400);
|
$response->assertStatus(400);
|
||||||
$response->assertJSONFragment([
|
$response->assertJSONFragment([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
|
|||||||
@ -29,6 +29,11 @@ class PatientCheckTest extends CIUnitTestCase
|
|||||||
|
|
||||||
protected $endpoint = 'api/patient/check';
|
protected $endpoint = 'api/patient/check';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Case 1: Check existing PatientID
|
* Test Case 1: Check existing PatientID
|
||||||
* Expected: 200 OK with data: false (already exists)
|
* Expected: 200 OK with data: false (already exists)
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class PatientCreateTest extends CIUnitTestCase
|
|||||||
use FeatureTestTrait;
|
use FeatureTestTrait;
|
||||||
protected $endpoint = 'api/patient';
|
protected $endpoint = 'api/patient';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
// 400 - Passed
|
// 400 - Passed
|
||||||
// Validation Gagal - Array Tidak Complete
|
// Validation Gagal - Array Tidak Complete
|
||||||
public function testCreatePatientValidationFail() {
|
public function testCreatePatientValidationFail() {
|
||||||
|
|||||||
@ -28,6 +28,11 @@ class PatientDeleteTest extends CIUnitTestCase
|
|||||||
|
|
||||||
protected $endpoint = 'api/patient';
|
protected $endpoint = 'api/patient';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Case 1: Delete without InternalPID key
|
* Test Case 1: Delete without InternalPID key
|
||||||
* Expected: 500 Internal Server Error (Undefined array key)
|
* Expected: 500 Internal Server Error (Undefined array key)
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class PatientIndexTest extends CIUnitTestCase
|
|||||||
|
|
||||||
protected $endpoint = 'api/patient';
|
protected $endpoint = 'api/patient';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Case 1: tanpa parameter, harus 200 dan status success - Passed
|
* Case 1: tanpa parameter, harus 200 dan status success - Passed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class PatientShowTest extends CIUnitTestCase
|
|||||||
|
|
||||||
protected $endpoint = 'api/patient';
|
protected $endpoint = 'api/patient';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
// 200 ok found - Passed
|
// 200 ok found - Passed
|
||||||
public function testShowSingleRow() {
|
public function testShowSingleRow() {
|
||||||
// Pastikan DB test punya seed patient InternalPID=10 tanpa id/addr
|
// Pastikan DB test punya seed patient InternalPID=10 tanpa id/addr
|
||||||
|
|||||||
@ -10,6 +10,11 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
{
|
{
|
||||||
use FeatureTestTrait;
|
use FeatureTestTrait;
|
||||||
protected $endpoint = 'api/patient';
|
protected $endpoint = 'api/patient';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 400 - Validation Fail
|
* 400 - Validation Fail
|
||||||
* Coba update tanpa field wajib → harus gagal validasi.
|
* Coba update tanpa field wajib → harus gagal validasi.
|
||||||
@ -17,7 +22,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
public function testUpdatePatientValidationFail()
|
public function testUpdatePatientValidationFail()
|
||||||
{
|
{
|
||||||
$payload = [ 'InternalPID' => null, 'NameFirst' => '' ]; // Tidak valid
|
$payload = [ 'InternalPID' => null, 'NameFirst' => '' ]; // Tidak valid
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
|
|
||||||
$result->assertStatus(400);
|
$result->assertStatus(400);
|
||||||
$json = $result->getJSON();
|
$json = $result->getJSON();
|
||||||
@ -34,7 +39,6 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$faker = Factory::create('id_ID');
|
$faker = Factory::create('id_ID');
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPID' => 999999, // Asumsi tidak ada di DB
|
|
||||||
"PatientID" => "SMAJ1",
|
"PatientID" => "SMAJ1",
|
||||||
"EmailAddress1" => 'asaas7890@gmail.com',
|
"EmailAddress1" => 'asaas7890@gmail.com',
|
||||||
"Phone" => $faker->numerify('08##########'),
|
"Phone" => $faker->numerify('08##########'),
|
||||||
@ -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)
|
$result->assertStatus(201); // Update returns success even if no rows found (depending on logic)
|
||||||
}
|
}
|
||||||
@ -66,7 +70,6 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
// NOTE: Sebaiknya ambil InternalPID yang sudah ada (mock atau dari DB fixture)
|
// NOTE: Sebaiknya ambil InternalPID yang sudah ada (mock atau dari DB fixture)
|
||||||
// Untuk contoh ini kita asumsikan ada ID 1
|
// Untuk contoh ini kita asumsikan ada ID 1
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPID' => 1,
|
|
||||||
"PatientID" => "SMAJ1",
|
"PatientID" => "SMAJ1",
|
||||||
'NameFirst' => $faker->firstName,
|
'NameFirst' => $faker->firstName,
|
||||||
'NameMiddle' => $faker->firstName,
|
'NameMiddle' => $faker->firstName,
|
||||||
@ -95,7 +98,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$payload['DeathDateTime'] = null;
|
$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);
|
$result->assertStatus(201);
|
||||||
$json = $result->getJSON();
|
$json = $result->getJSON();
|
||||||
$data = json_decode($json, true);
|
$data = json_decode($json, true);
|
||||||
@ -110,7 +113,6 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$faker = Factory::create('id_ID');
|
$faker = Factory::create('id_ID');
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPID' => 1,
|
|
||||||
"PatientID" => "SMAJ1",
|
"PatientID" => "SMAJ1",
|
||||||
'NameFirst' => $faker->firstName,
|
'NameFirst' => $faker->firstName,
|
||||||
'NameMiddle' => $faker->firstName,
|
'NameMiddle' => $faker->firstName,
|
||||||
@ -138,7 +140,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$payload['DeathDateTime'] = null;
|
$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);
|
$result->assertStatus(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +152,6 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$faker = Factory::create('id_ID');
|
$faker = Factory::create('id_ID');
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPID' => 1,
|
|
||||||
"PatientID" => "SMAJ1",
|
"PatientID" => "SMAJ1",
|
||||||
'NameFirst' => $faker->firstName,
|
'NameFirst' => $faker->firstName,
|
||||||
'NameMiddle' => $faker->firstName,
|
'NameMiddle' => $faker->firstName,
|
||||||
@ -176,7 +177,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$payload['DeathDateTime'] = null;
|
$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);
|
$result->assertStatus(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +189,6 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$faker = Factory::create('id_ID');
|
$faker = Factory::create('id_ID');
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPID' => 1,
|
|
||||||
"PatientID" => "SMAJ1",
|
"PatientID" => "SMAJ1",
|
||||||
'NameFirst' => $faker->firstName,
|
'NameFirst' => $faker->firstName,
|
||||||
'NameMiddle' => $faker->firstName,
|
'NameMiddle' => $faker->firstName,
|
||||||
@ -216,7 +216,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$payload['DeathDateTime'] = null;
|
$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);
|
$result->assertStatus(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +228,6 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$faker = Factory::create('id_ID');
|
$faker = Factory::create('id_ID');
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'InternalPID' => 1,
|
|
||||||
"PatientID" => "SMAJ1",
|
"PatientID" => "SMAJ1",
|
||||||
'NameFirst' => $faker->firstName,
|
'NameFirst' => $faker->firstName,
|
||||||
'NameMiddle' => $faker->firstName,
|
'NameMiddle' => $faker->firstName,
|
||||||
@ -256,7 +255,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
$payload['DeathDateTime'] = null;
|
$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);
|
$result->assertStatus(500);
|
||||||
$json = $result->getJSON();
|
$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');
|
$values = array_column($data['data'], 'value');
|
||||||
$labels = array_column($data['data'], 'label');
|
$labels = array_column($data['data'], 'label');
|
||||||
|
|
||||||
$this->assertContains('1', $values);
|
$this->assertContains('F', $values);
|
||||||
$this->assertContains('2', $values);
|
$this->assertContains('M', $values);
|
||||||
$this->assertContains('Female', $labels);
|
$this->assertContains('Female', $labels);
|
||||||
$this->assertContains('Male', $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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -28,9 +28,9 @@ class ValueSetTest extends CIUnitTestCase
|
|||||||
$values = array_column($result, 'value');
|
$values = array_column($result, 'value');
|
||||||
$labels = array_column($result, 'label');
|
$labels = array_column($result, 'label');
|
||||||
|
|
||||||
$this->assertContains('1', $values);
|
$this->assertContains('F', $values);
|
||||||
$this->assertContains('2', $values);
|
$this->assertContains('M', $values);
|
||||||
$this->assertContains('3', $values);
|
$this->assertContains('U', $values);
|
||||||
$this->assertContains('Female', $labels);
|
$this->assertContains('Female', $labels);
|
||||||
$this->assertContains('Male', $labels);
|
$this->assertContains('Male', $labels);
|
||||||
$this->assertContains('Unknown', $labels);
|
$this->assertContains('Unknown', $labels);
|
||||||
@ -51,17 +51,17 @@ class ValueSetTest extends CIUnitTestCase
|
|||||||
$keys = array_column($result, 'key');
|
$keys = array_column($result, 'key');
|
||||||
$values = array_column($result, 'value');
|
$values = array_column($result, 'value');
|
||||||
|
|
||||||
$this->assertContains('1', $keys);
|
$this->assertContains('F', $keys);
|
||||||
$this->assertContains('2', $keys);
|
$this->assertContains('M', $keys);
|
||||||
$this->assertContains('Female', $values);
|
$this->assertContains('Female', $values);
|
||||||
$this->assertContains('Male', $values);
|
$this->assertContains('Male', $values);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetLabelConvertsCodeToLabel()
|
public function testGetLabelConvertsCodeToLabel()
|
||||||
{
|
{
|
||||||
$this->assertEquals('Female', ValueSet::getLabel('sex', '1'));
|
$this->assertEquals('Female', ValueSet::getLabel('sex', 'F'));
|
||||||
$this->assertEquals('Male', ValueSet::getLabel('sex', '2'));
|
$this->assertEquals('Male', ValueSet::getLabel('sex', 'M'));
|
||||||
$this->assertEquals('Unknown', ValueSet::getLabel('sex', '3'));
|
$this->assertEquals('Unknown', ValueSet::getLabel('sex', 'U'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetLabelForOrderPriority()
|
public function testGetLabelForOrderPriority()
|
||||||
@ -118,9 +118,9 @@ class ValueSetTest extends CIUnitTestCase
|
|||||||
public function testGetPatientSex()
|
public function testGetPatientSex()
|
||||||
{
|
{
|
||||||
$result = ValueSet::get('sex');
|
$result = ValueSet::get('sex');
|
||||||
$this->assertEquals('1', $result[0]['value']);
|
$this->assertEquals('F', $result[0]['value']);
|
||||||
$this->assertEquals('Female', $result[0]['label']);
|
$this->assertEquals('Female', $result[0]['label']);
|
||||||
$this->assertEquals('2', $result[1]['value']);
|
$this->assertEquals('M', $result[1]['value']);
|
||||||
$this->assertEquals('Male', $result[1]['label']);
|
$this->assertEquals('Male', $result[1]['label']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,9 +270,9 @@ class ValueSetTest extends CIUnitTestCase
|
|||||||
public function testGetReturnsFormattedValues()
|
public function testGetReturnsFormattedValues()
|
||||||
{
|
{
|
||||||
$result = ValueSet::get('sex');
|
$result = ValueSet::get('sex');
|
||||||
$this->assertEquals('1', $result[0]['value']);
|
$this->assertEquals('F', $result[0]['value']);
|
||||||
$this->assertEquals('Female', $result[0]['label']);
|
$this->assertEquals('Female', $result[0]['label']);
|
||||||
$this->assertEquals('2', $result[1]['value']);
|
$this->assertEquals('M', $result[1]['value']);
|
||||||
$this->assertEquals('Male', $result[1]['label']);
|
$this->assertEquals('Male', $result[1]['label']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,8 +355,8 @@ class ValueSetTest extends CIUnitTestCase
|
|||||||
public function testTransformLabels()
|
public function testTransformLabels()
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
['Gender' => '1', 'Country' => 'IDN'],
|
['Gender' => 'F', 'Country' => 'IDN'],
|
||||||
['Gender' => '2', 'Country' => 'USA']
|
['Gender' => 'M', 'Country' => 'USA']
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = ValueSet::transformLabels($data, [
|
$result = ValueSet::transformLabels($data, [
|
||||||
@ -364,9 +364,9 @@ class ValueSetTest extends CIUnitTestCase
|
|||||||
'Country' => 'country'
|
'Country' => 'country'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('1', $result[0]['Gender']);
|
$this->assertEquals('F', $result[0]['Gender']);
|
||||||
$this->assertEquals('Female', $result[0]['GenderLabel']);
|
$this->assertEquals('Female', $result[0]['GenderLabel']);
|
||||||
$this->assertEquals('2', $result[1]['Gender']);
|
$this->assertEquals('M', $result[1]['Gender']);
|
||||||
$this->assertEquals('Male', $result[1]['GenderLabel']);
|
$this->assertEquals('Male', $result[1]['GenderLabel']);
|
||||||
$this->assertEquals('USA', $result[1]['Country']);
|
$this->assertEquals('USA', $result[1]['Country']);
|
||||||
$this->assertEquals('United States of America', $result[1]['CountryLabel']);
|
$this->assertEquals('United States of America', $result[1]['CountryLabel']);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user