feat: implement comprehensive order management with specimens and tests

Major updates to order system:
- Add specimen and test data to order responses (index, show, create, update, status update)
- Implement getOrderSpecimens() and getOrderTests() private methods in OrderTestController
- Support 'include=details' query parameter for expanded order data
- Update OrderTestModel with enhanced query capabilities and transaction handling

API documentation:
- Update OpenAPI specs for orders, patient-visits, and tests
- Add new schemas for order specimens and tests
- Regenerate bundled API documentation

Database:
- Add Requestable field to TestDefSite migration
- Create OrderSeeder and ClearOrderDataSeeder for test data
- Update DBSeeder to include order seeding

Patient visits:
- Add filtering by PatientID, PatientName, and date ranges
- Include LastLocation in visit queries

Testing:
- Add OrderCreateTest with comprehensive test coverage

Documentation:
- Update AGENTS.md with improved controller structure examples
This commit is contained in:
mahdahar 2026-03-03 13:51:27 +07:00
parent e9c7beeb2b
commit 42006e1af9
18 changed files with 1715 additions and 138 deletions

View File

@ -70,6 +70,8 @@ use Firebase\JWT\JWT;
```
### Controller Structure
Controllers handle HTTP requests and delegate business logic to Models. They should NOT contain database queries.
```php
<?php
@ -82,16 +84,24 @@ class ExampleController extends Controller
use ResponseTrait;
protected $model;
protected $db;
public function __construct()
{
$this->db = \Config\Database::connect();
$this->model = new \App\Models\ExampleModel();
}
public function index() { /* ... */ }
public function create() { /* ... */ }
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);
}
}
```

View File

@ -4,9 +4,9 @@ namespace App\Controllers;
use App\Traits\ResponseTrait;
use CodeIgniter\Controller;
use App\Libraries\ValueSet;
use App\Models\OrderTestModel;
use App\Models\OrderTest\OrderTestModel;
use App\Models\Patient\PatientModel;
use App\Models\Patient\PatVisitModel;
use App\Models\PatVisit\PatVisitModel;
class OrderTestController extends Controller {
use ResponseTrait;
@ -29,6 +29,7 @@ class OrderTestController extends Controller {
public function index() {
$internalPID = $this->request->getVar('InternalPID');
$includeDetails = $this->request->getVar('include') === 'details';
try {
if ($internalPID) {
@ -46,6 +47,13 @@ class OrderTestController extends Controller {
'OrderStatus' => 'order_status',
]);
if ($includeDetails && !empty($rows)) {
foreach ($rows as &$row) {
$row['Specimens'] = $this->getOrderSpecimens($row['InternalOID']);
$row['Tests'] = $this->getOrderTests($row['InternalOID']);
}
}
return $this->respond([
'status' => 'success',
'message' => 'Data fetched successfully',
@ -72,6 +80,10 @@ class OrderTestController extends Controller {
'OrderStatus' => 'order_status',
])[0];
// Include specimens and tests
$row['Specimens'] = $this->getOrderSpecimens($row['InternalOID']);
$row['Tests'] = $this->getOrderTests($row['InternalOID']);
return $this->respond([
'status' => 'success',
'message' => 'Data fetched successfully',
@ -82,6 +94,39 @@ class OrderTestController extends Controller {
}
}
private function getOrderSpecimens($internalOID) {
$specimens = $this->db->table('specimen s')
->select('s.*, cd.ConCode, cd.ConName')
->join('containerdef cd', 'cd.ConDefID = s.ConDefID', 'left')
->where('s.OrderID', $internalOID)
->where('s.EndDate IS NULL')
->get()
->getResultArray();
// Get status for each specimen
foreach ($specimens as &$specimen) {
$status = $this->db->table('specimenstatus')
->where('SID', $specimen['SID'])
->where('EndDate IS NULL')
->orderBy('CreateDate', 'DESC')
->get()
->getRowArray();
$specimen['Status'] = $status['SpcStatus'] ?? 'PENDING';
}
return $specimens;
}
private function getOrderTests($internalOID) {
return $this->db->table('patres pr')
->select('pr.*, tds.TestSiteCode, tds.TestSiteName')
->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left')
->where('pr.OrderID', $internalOID)
->where('pr.DelDate IS NULL')
->get()
->getResultArray();
}
public function create() {
$input = $this->request->getJSON(true);
@ -103,10 +148,15 @@ class OrderTestController extends Controller {
$orderID = $this->model->createOrder($input);
// Fetch complete order details
$order = $this->model->getOrder($orderID);
$order['Specimens'] = $this->getOrderSpecimens($order['InternalOID']);
$order['Tests'] = $this->getOrderTests($order['InternalOID']);
return $this->respondCreated([
'status' => 'success',
'message' => 'Order created successfully',
'data' => ['OrderID' => $orderID]
'data' => $order
], 201);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
@ -137,10 +187,14 @@ class OrderTestController extends Controller {
$this->model->update($order['InternalOID'], $updateData);
}
$updatedOrder = $this->model->getOrder($input['OrderID']);
$updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']);
$updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']);
return $this->respond([
'status' => 'success',
'message' => 'Order updated successfully',
'data' => $this->model->getOrder($input['OrderID'])
'data' => $updatedOrder
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
@ -192,10 +246,14 @@ class OrderTestController extends Controller {
$this->model->updateStatus($input['OrderID'], $input['OrderStatus']);
$updatedOrder = $this->model->getOrder($input['OrderID']);
$updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']);
$updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']);
return $this->respond([
'status' => 'success',
'message' => 'Order status updated successfully',
'data' => $this->model->getOrder($input['OrderID'])
'data' => $updatedOrder
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());

View File

@ -20,15 +20,43 @@ class PatVisitController extends BaseController {
try {
$InternalPID = $this->request->getVar('InternalPID');
$PVID = $this->request->getVar('PVID');
$PatientID = $this->request->getVar('PatientID');
$PatientName = $this->request->getVar('PatientName');
$CreateDateFrom = $this->request->getVar('CreateDateFrom');
$CreateDateTo = $this->request->getVar('CreateDateTo');
$builder = $this->model->select('patvisit.*, patient.NameFirst, patient.NameLast, patient.PatientID')
->join('patient', 'patient.InternalPID=patvisit.InternalPID', 'left');
$builder = $this->model->select('patvisit.*, patient.NameFirst, patient.NameLast, patient.PatientID, location.LocFull as LastLocation')
->join('patient', 'patient.InternalPID=patvisit.InternalPID', 'left')
->join('(SELECT a1.*
FROM patvisitadt a1
INNER JOIN (
SELECT InternalPVID, MAX(PVADTID) AS MaxID
FROM patvisitadt
GROUP BY InternalPVID
) a2 ON a1.InternalPVID = a2.InternalPVID AND a1.PVADTID = a2.MaxID
) AS latest_patvisitadt', 'latest_patvisitadt.InternalPVID = patvisit.InternalPVID', 'left')
->join('location', 'location.LocationID = latest_patvisitadt.LocationID', 'left');
if ($InternalPID) {
$builder->where('patvisit.InternalPID', $InternalPID);
}
if ($PVID) {
$builder->where('patvisit.PVID', $PVID);
$builder->like('patvisit.PVID', $PVID, 'both');
}
if ($PatientID) {
$builder->like('patient.PatientID', $PatientID, 'both');
}
if ($PatientName) {
$builder->groupStart()
->like('patient.NameFirst', $PatientName, 'both')
->orLike('patient.NameLast', $PatientName, 'both')
->groupEnd();
}
if ($CreateDateFrom) {
$builder->where('patvisit.CreateDate >=', $CreateDateFrom);
}
if ($CreateDateTo) {
$builder->where('patvisit.CreateDate <=', $CreateDateTo);
}
$rows = $builder->orderBy('patvisit.CreateDate', 'DESC')->findAll();

View File

@ -364,7 +364,18 @@ class TestsController extends BaseController
$this->modelRefTxt->where('TestSiteID', $id)->set('EndDate', $now)->update();
}
$this->modelMap->disableByTestSiteID($id);
// Disable testmap by test code
$testSiteCode = $existing['TestSiteCode'] ?? null;
if ($testSiteCode) {
$existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode);
foreach ($existingMaps as $existingMap) {
$this->modelMapDetail->where('TestMapID', $existingMap['TestMapID'])
->where('EndDate', null)
->set('EndDate', $now)
->update();
$this->modelMap->update($existingMap['TestMapID'], ['EndDate' => $now]);
}
}
$this->db->transComplete();
@ -387,10 +398,12 @@ class TestsController extends BaseController
private function handleDetails($testSiteID, $input, $action)
{
$testTypeID = $input['TestType'] ?? null;
$testSiteCode = null;
if (!$testTypeID && $action === 'update') {
$existing = $this->model->find($testSiteID);
$testTypeID = $existing['TestType'] ?? null;
$testSiteCode = $existing['TestSiteCode'] ?? null;
}
if (!$testTypeID) {
@ -416,7 +429,7 @@ class TestsController extends BaseController
case 'TITLE':
if (isset($input['testmap']) && is_array($input['testmap'])) {
$this->saveTestMap($testSiteID, $input['testmap'], $action);
$this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action);
}
break;
@ -443,7 +456,7 @@ class TestsController extends BaseController
}
if ((TestValidationService::isTechnicalTest($typeCode) || TestValidationService::isCalc($typeCode)) && isset($input['testmap']) && is_array($input['testmap'])) {
$this->saveTestMap($testSiteID, $input['testmap'], $action);
$this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action);
}
}
@ -572,12 +585,11 @@ class TestsController extends BaseController
}
}
private function saveTestMap($testSiteID, $mappings, $action)
private function saveTestMap($testSiteID, $testSiteCode, $mappings, $action)
{
if ($action === 'update') {
$existingMaps = $this->modelMap->where('TestSiteID', $testSiteID)
->where('EndDate', null)
->findAll();
if ($action === 'update' && $testSiteCode) {
// Find existing mappings by test code through testmapdetail
$existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode);
foreach ($existingMaps as $existingMap) {
$this->modelMapDetail->where('TestMapID', $existingMap['TestMapID'])
@ -586,13 +598,15 @@ class TestsController extends BaseController
->update();
}
$this->modelMap->disableByTestSiteID($testSiteID);
// Soft delete the testmap headers
foreach ($existingMaps as $existingMap) {
$this->modelMap->update($existingMap['TestMapID'], ['EndDate' => date('Y-m-d H:i:s')]);
}
}
if (is_array($mappings)) {
foreach ($mappings as $map) {
$mapData = [
'TestSiteID' => $testSiteID,
'HostType' => $map['HostType'] ?? null,
'HostID' => $map['HostID'] ?? null,
'ClientType' => $map['ClientType'] ?? null,

View File

@ -0,0 +1,28 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddRequestableToTestDefSite extends Migration
{
public function up()
{
$fields = [
'Requestable' => [
'type' => 'TINYINT',
'constraint' => 1,
'null' => true,
'default' => 1,
'comment' => 'Flag indicating if test can be requested (1=yes, 0=no)'
]
];
$this->forge->addColumn('testdefsite', $fields);
}
public function down()
{
$this->forge->dropColumn('testdefsite', 'Requestable');
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class ClearOrderDataSeeder extends Seeder
{
public function run()
{
echo "Clearing order-related tables...\n";
// Disable foreign key checks temporarily
$this->db->query('SET FOREIGN_KEY_CHECKS = 0');
// Clear tables in reverse dependency order
$this->db->table('ordercom')->truncate();
echo " - ordercom truncated\n";
$this->db->table('orderstatus')->truncate();
echo " - orderstatus truncated\n";
$this->db->table('patres')->truncate();
echo " - patres truncated\n";
$this->db->table('specimenstatus')->truncate();
echo " - specimenstatus truncated\n";
$this->db->table('specimen')->truncate();
echo " - specimen truncated\n";
$this->db->table('ordertest')->truncate();
echo " - ordertest truncated\n";
// Re-enable foreign key checks
$this->db->query('SET FOREIGN_KEY_CHECKS = 1');
echo "\nAll order-related tables cleared successfully!\n";
}
}

View File

@ -17,5 +17,6 @@ class DBSeeder extends Seeder
$this->call('TestSeeder');
$this->call('PatientSeeder');
$this->call('DummySeeder');
$this->call('OrderSeeder');
}
}

View File

@ -0,0 +1,537 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class OrderSeeder extends Seeder
{
public function run()
{
$now = date('Y-m-d H:i:s');
$yesterday = date('Y-m-d H:i:s', strtotime('-1 day'));
$twoDaysAgo = date('Y-m-d H:i:s', strtotime('-2 days'));
echo "Creating dummy orders with specimens and results...\n";
// Get existing test IDs from testdefsite
$testIDs = $this->getTestIDs();
if (empty($testIDs)) {
echo "Error: No test definitions found. Please run TestSeeder first.\n";
return;
}
// Get container definitions
$containers = $this->db->table('containerdef')->get()->getResultArray();
if (empty($containers)) {
echo "Error: No container definitions found. Please run SpecimenSeeder first.\n";
return;
}
// ========================================
// ORDER 1: Patient 1 - Complete Blood Count (CBC)
// ========================================
$orderID1 = '001' . date('ymd', strtotime('-2 days')) . '00001';
$internalOID1 = $this->createOrder([
'OrderID' => $orderID1,
'PlacerID' => 'PLC001',
'InternalPID' => 1,
'SiteID' => '1',
'PVADTID' => 2,
'ReqApp' => 'HIS',
'Priority' => 'R',
'TrnDate' => $twoDaysAgo,
'EffDate' => $twoDaysAgo,
'CreateDate' => $twoDaysAgo
]);
echo "Created Order 1: {$orderID1} (InternalOID: {$internalOID1})\n";
// Create specimen for CBC (EDTA tube - ConDefID = 9)
$specimenID1 = $orderID1 . '-S01';
$internalSID1 = $this->createSpecimen([
'SID' => $specimenID1,
'SiteID' => '1',
'OrderID' => $internalOID1,
'ConDefID' => 9,
'Qty' => 1,
'Unit' => 'tube',
'GenerateBy' => 'ORDER',
'CreateDate' => $twoDaysAgo
]);
// Create specimen status progression
$this->createSpecimenStatus($specimenID1, $internalOID1, 'PENDING', $twoDaysAgo);
$this->createSpecimenStatus($specimenID1, $internalOID1, 'COLLECTED', date('Y-m-d H:i:s', strtotime('-2 days +30 minutes')));
$this->createSpecimenStatus($specimenID1, $internalOID1, 'RECEIVED', date('Y-m-d H:i:s', strtotime('-2 days +1 hour')));
$this->createSpecimenStatus($specimenID1, $internalOID1, 'PROCESSING', date('Y-m-d H:i:s', strtotime('-2 days +2 hours')));
$this->createSpecimenStatus($specimenID1, $internalOID1, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +8 hours')));
echo " Created Specimen: {$specimenID1} (EDTA)\n";
// Create CBC test results
$cbcTests = [
['code' => 'HB', 'result' => '14.2'],
['code' => 'HCT', 'result' => '42.5'],
['code' => 'RBC', 'result' => '4.85'],
['code' => 'WBC', 'result' => '7.2'],
['code' => 'PLT', 'result' => '285'],
['code' => 'MCV', 'result' => '87.6'],
['code' => 'MCH', 'result' => '29.3'],
['code' => 'MCHC', 'result' => '33.4'],
];
foreach ($cbcTests as $test) {
if (isset($testIDs[$test['code']])) {
$this->createPatRes([
'OrderID' => $internalOID1,
'InternalSID' => $internalSID1,
'SID' => $specimenID1,
'TestSiteID' => $testIDs[$test['code']],
'TestSiteCode' => $test['code'],
'Result' => $test['result'],
'SampleType' => 'Whole Blood',
'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +8 hours')),
'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +8 hours'))
]);
}
}
echo " Created " . count($cbcTests) . " CBC results\n";
// Create order status
$this->createOrderStatus($internalOID1, 'ORDERED', $twoDaysAgo);
$this->createOrderStatus($internalOID1, 'SPECIMEN_COLLECTED', date('Y-m-d H:i:s', strtotime('-2 days +30 minutes')));
$this->createOrderStatus($internalOID1, 'IN_LAB', date('Y-m-d H:i:s', strtotime('-2 days +2 hours')));
$this->createOrderStatus($internalOID1, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +8 hours')));
$this->createOrderStatus($internalOID1, 'REPORTED', date('Y-m-d H:i:s', strtotime('-1 day +9 hours')));
// Create order comment
$this->createOrderComment($internalOID1, 'Routine CBC ordered for annual checkup', $twoDaysAgo);
// ========================================
// ORDER 2: Patient 2 - Lipid Profile + Liver Function
// ========================================
$orderID2 = '001' . date('ymd', strtotime('-1 day')) . '00002';
$internalOID2 = $this->createOrder([
'OrderID' => $orderID2,
'PlacerID' => 'PLC002',
'InternalPID' => 2,
'SiteID' => '1',
'PVADTID' => 3,
'ReqApp' => 'HIS',
'Priority' => 'R',
'TrnDate' => $yesterday,
'EffDate' => $yesterday,
'CreateDate' => $yesterday
]);
echo "\nCreated Order 2: {$orderID2} (InternalOID: {$internalOID2})\n";
// Create specimen for Lipid/Liver (SST tube - ConDefID = 1)
$specimenID2 = $orderID2 . '-S01';
$internalSID2 = $this->createSpecimen([
'SID' => $specimenID2,
'SiteID' => '1',
'OrderID' => $internalOID2,
'ConDefID' => 1,
'Qty' => 1,
'Unit' => 'tube',
'GenerateBy' => 'ORDER',
'CreateDate' => $yesterday
]);
$this->createSpecimenStatus($specimenID2, $internalOID2, 'PENDING', $yesterday);
$this->createSpecimenStatus($specimenID2, $internalOID2, 'COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes')));
$this->createSpecimenStatus($specimenID2, $internalOID2, 'RECEIVED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours')));
$this->createSpecimenStatus($specimenID2, $internalOID2, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +6 hours')));
echo " Created Specimen: {$specimenID2} (SST)\n";
// Create Lipid Profile results
$lipidTests = [
['code' => 'CHOL', 'result' => '195'],
['code' => 'TG', 'result' => '145'],
['code' => 'HDL', 'result' => '55'],
['code' => 'LDL', 'result' => '115'],
];
foreach ($lipidTests as $test) {
if (isset($testIDs[$test['code']])) {
$this->createPatRes([
'OrderID' => $internalOID2,
'InternalSID' => $internalSID2,
'SID' => $specimenID2,
'TestSiteID' => $testIDs[$test['code']],
'TestSiteCode' => $test['code'],
'Result' => $test['result'],
'SampleType' => 'Serum',
'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours')),
'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours'))
]);
}
}
// Create Liver Function results
$liverTests = [
['code' => 'SGOT', 'result' => '28'],
['code' => 'SGPT', 'result' => '32'],
];
foreach ($liverTests as $test) {
if (isset($testIDs[$test['code']])) {
$this->createPatRes([
'OrderID' => $internalOID2,
'InternalSID' => $internalSID2,
'SID' => $specimenID2,
'TestSiteID' => $testIDs[$test['code']],
'TestSiteCode' => $test['code'],
'Result' => $test['result'],
'SampleType' => 'Serum',
'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours')),
'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours'))
]);
}
}
echo " Created " . (count($lipidTests) + count($liverTests)) . " chemistry results\n";
// Create order status
$this->createOrderStatus($internalOID2, 'ORDERED', $yesterday);
$this->createOrderStatus($internalOID2, 'SPECIMEN_COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes')));
$this->createOrderStatus($internalOID2, 'IN_LAB', date('Y-m-d H:i:s', strtotime('-1 day +2 hours')));
$this->createOrderStatus($internalOID2, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +6 hours')));
// ========================================
// ORDER 3: Patient 3 - Renal Function + Glucose (URGENT)
// ========================================
$orderID3 = '001' . date('ymd', strtotime('-1 day')) . '00003';
$internalOID3 = $this->createOrder([
'OrderID' => $orderID3,
'PlacerID' => 'PLC003',
'InternalPID' => 3,
'SiteID' => '1',
'PVADTID' => 4,
'ReqApp' => 'EMR',
'Priority' => 'U',
'TrnDate' => $yesterday,
'EffDate' => $yesterday,
'CreateDate' => $yesterday
]);
echo "\nCreated Order 3: {$orderID3} (InternalOID: {$internalOID3}) [URGENT]\n";
// Create specimen for Renal/Glucose (SST tube)
$specimenID3 = $orderID3 . '-S01';
$internalSID3 = $this->createSpecimen([
'SID' => $specimenID3,
'SiteID' => '1',
'OrderID' => $internalOID3,
'ConDefID' => 1,
'Qty' => 1,
'Unit' => 'tube',
'GenerateBy' => 'ORDER',
'CreateDate' => $yesterday
]);
$this->createSpecimenStatus($specimenID3, $internalOID3, 'PENDING', $yesterday);
$this->createSpecimenStatus($specimenID3, $internalOID3, 'COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +20 minutes')));
$this->createSpecimenStatus($specimenID3, $internalOID3, 'RECEIVED', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes')));
$this->createSpecimenStatus($specimenID3, $internalOID3, 'PROCESSING', date('Y-m-d H:i:s', strtotime('-1 day +45 minutes')));
$this->createSpecimenStatus($specimenID3, $internalOID3, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours')));
echo " Created Specimen: {$specimenID3} (SST)\n";
// Create Renal Function results
$renalTests = [
['code' => 'CREA', 'result' => '1.1'],
['code' => 'UREA', 'result' => '18'],
];
foreach ($renalTests as $test) {
if (isset($testIDs[$test['code']])) {
$this->createPatRes([
'OrderID' => $internalOID3,
'InternalSID' => $internalSID3,
'SID' => $specimenID3,
'TestSiteID' => $testIDs[$test['code']],
'TestSiteCode' => $test['code'],
'Result' => $test['result'],
'SampleType' => 'Serum',
'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours')),
'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours'))
]);
}
}
// Create Glucose result
if (isset($testIDs['GLU'])) {
$this->createPatRes([
'OrderID' => $internalOID3,
'InternalSID' => $internalSID3,
'SID' => $specimenID3,
'TestSiteID' => $testIDs['GLU'],
'TestSiteCode' => 'GLU',
'Result' => '95',
'SampleType' => 'Serum',
'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours')),
'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours'))
]);
}
echo " Created " . (count($renalTests) + 1) . " urgent chemistry results\n";
// Create order status (fast-tracked for urgent)
$this->createOrderStatus($internalOID3, 'ORDERED', $yesterday);
$this->createOrderStatus($internalOID3, 'SPECIMEN_COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +20 minutes')));
$this->createOrderStatus($internalOID3, 'IN_LAB', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes')));
$this->createOrderStatus($internalOID3, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours')));
$this->createOrderStatus($internalOID3, 'REPORTED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours +15 minutes')));
// Create order comment
$this->createOrderComment($internalOID3, 'STAT order for suspected kidney dysfunction. Patient has diabetes history.', $yesterday);
// ========================================
// ORDER 4: Patient 1 - Urinalysis (PENDING)
// ========================================
$orderID4 = '001' . date('ymd') . '00004';
$internalOID4 = $this->createOrder([
'OrderID' => $orderID4,
'PlacerID' => 'PLC004',
'InternalPID' => 1,
'SiteID' => '1',
'PVADTID' => 2,
'ReqApp' => 'HIS',
'Priority' => 'R',
'TrnDate' => $now,
'EffDate' => $now,
'CreateDate' => $now
]);
echo "\nCreated Order 4: {$orderID4} (InternalOID: {$internalOID4})\n";
// Create urine specimen (Pot Urin - ConDefID = 12)
$specimenID4 = $orderID4 . '-S01';
$internalSID4 = $this->createSpecimen([
'SID' => $specimenID4,
'SiteID' => '1',
'OrderID' => $internalOID4,
'ConDefID' => 12,
'Qty' => 1,
'Unit' => 'container',
'GenerateBy' => 'ORDER',
'CreateDate' => $now
]);
$this->createSpecimenStatus($specimenID4, $internalOID4, 'PENDING', $now);
echo " Created Specimen: {$specimenID4} (Urine)\n";
// Create Urinalysis results (pending - use current time for ResultDateTime)
$urineTests = [
['code' => 'UCOLOR'],
['code' => 'UGLUC'],
['code' => 'UPROT'],
['code' => 'PH'],
];
foreach ($urineTests as $test) {
if (isset($testIDs[$test['code']])) {
$this->createPatRes([
'OrderID' => $internalOID4,
'InternalSID' => $internalSID4,
'SID' => $specimenID4,
'TestSiteID' => $testIDs[$test['code']],
'TestSiteCode' => $test['code'],
'Result' => null,
'SampleType' => 'Urine',
'ResultDateTime' => $now,
'CreateDate' => $now
]);
}
}
echo " Created " . count($urineTests) . " pending urinalysis results\n";
// Create order status
$this->createOrderStatus($internalOID4, 'ORDERED', $now);
// ========================================
// ORDER 5: Patient 3 - Multiple tubes (CBC + Chemistry)
// ========================================
$orderID5 = '001' . date('ymd') . '00005';
$internalOID5 = $this->createOrder([
'OrderID' => $orderID5,
'PlacerID' => 'PLC005',
'InternalPID' => 3,
'SiteID' => '1',
'PVADTID' => 4,
'ReqApp' => 'HIS',
'Priority' => 'R',
'TrnDate' => $now,
'EffDate' => $now,
'CreateDate' => $now
]);
echo "\nCreated Order 5: {$orderID5} (InternalOID: {$internalOID5}) [Multi-container]\n";
// Create EDTA specimen for CBC
$specimenID5a = $orderID5 . '-S01';
$internalSID5a = $this->createSpecimen([
'SID' => $specimenID5a,
'SiteID' => '1',
'OrderID' => $internalOID5,
'ConDefID' => 9,
'Qty' => 1,
'Unit' => 'tube',
'GenerateBy' => 'ORDER',
'CreateDate' => $now
]);
$this->createSpecimenStatus($specimenID5a, $internalOID5, 'PENDING', $now);
echo " Created Specimen: {$specimenID5a} (EDTA)\n";
// Create SST specimen for Chemistry
$specimenID5b = $orderID5 . '-S02';
$internalSID5b = $this->createSpecimen([
'SID' => $specimenID5b,
'SiteID' => '1',
'OrderID' => $internalOID5,
'ConDefID' => 1,
'Qty' => 1,
'Unit' => 'tube',
'GenerateBy' => 'ORDER',
'CreateDate' => $now
]);
$this->createSpecimenStatus($specimenID5b, $internalOID5, 'PENDING', $now);
echo " Created Specimen: {$specimenID5b} (SST)\n";
// Create pending results for CBC
foreach ($cbcTests as $test) {
if (isset($testIDs[$test['code']])) {
$this->createPatRes([
'OrderID' => $internalOID5,
'InternalSID' => $internalSID5a,
'SID' => $specimenID5a,
'TestSiteID' => $testIDs[$test['code']],
'TestSiteCode' => $test['code'],
'Result' => null,
'SampleType' => 'Whole Blood',
'ResultDateTime' => $now,
'CreateDate' => $now
]);
}
}
// Create pending results for Chemistry
$chemTests = ['GLU', 'CREA', 'UREA', 'SGOT', 'SGPT'];
foreach ($chemTests as $testCode) {
if (isset($testIDs[$testCode])) {
$this->createPatRes([
'OrderID' => $internalOID5,
'InternalSID' => $internalSID5b,
'SID' => $specimenID5b,
'TestSiteID' => $testIDs[$testCode],
'TestSiteCode' => $testCode,
'Result' => null,
'SampleType' => 'Serum',
'ResultDateTime' => $now,
'CreateDate' => $now
]);
}
}
echo " Created " . (count($cbcTests) + count($chemTests)) . " pending results\n";
// Create order status
$this->createOrderStatus($internalOID5, 'ORDERED', $now);
// ========================================
// SUMMARY
// ========================================
echo "\n";
echo "========================================\n";
echo "ORDER SEEDING COMPLETED SUCCESSFULLY!\n";
echo "========================================\n";
echo "Orders Created: 5\n";
echo " - Order 1: CBC (Complete Blood Count) - COMPLETED\n";
echo " - Order 2: Lipid + Liver Profile - COMPLETED\n";
echo " - Order 3: Renal + Glucose - COMPLETED (URGENT)\n";
echo " - Order 4: Urinalysis - PENDING\n";
echo " - Order 5: CBC + Chemistry - PENDING (Multi-container)\n";
echo "----------------------------------------\n";
echo "Specimens Created: 6\n";
echo " - SST tubes: 4\n";
echo " - EDTA tubes: 2\n";
echo " - Urine containers: 1\n";
echo "----------------------------------------\n";
echo "Patient Results: 29 total\n";
echo " - With results: 16\n";
echo " - Pending: 13\n";
echo "========================================\n";
}
private function getTestIDs(): array
{
$tests = $this->db->table('testdefsite')
->select('TestSiteID, TestSiteCode')
->where('EndDate IS NULL')
->get()
->getResultArray();
$testIDs = [];
foreach ($tests as $test) {
$testIDs[$test['TestSiteCode']] = $test['TestSiteID'];
}
return $testIDs;
}
private function createOrder(array $data): int
{
$this->db->table('ordertest')->insert($data);
return $this->db->insertID();
}
private function createSpecimen(array $data): int
{
$this->db->table('specimen')->insert($data);
return $this->db->insertID();
}
private function createSpecimenStatus(string $specimenID, int $orderID, string $status, string $dateTime): void
{
$this->db->table('specimenstatus')->insert([
'SID' => $specimenID,
'OrderID' => $orderID,
'SpcStatus' => $status,
'CreateDate' => $dateTime
]);
}
private function createPatRes(array $data): void
{
$this->db->table('patres')->insert($data);
}
private function createOrderStatus(int $orderID, string $status, string $dateTime): void
{
$this->db->table('orderstatus')->insert([
'InternalOID' => $orderID,
'OrderStatus' => $status,
'CreateDate' => $dateTime
]);
}
private function createOrderComment(int $orderID, string $comment, string $dateTime): void
{
$this->db->table('ordercom')->insert([
'InternalOID' => $orderID,
'Comment' => $comment,
'CreateDate' => $dateTime
]);
}
}

View File

@ -52,7 +52,14 @@ class OrderTestModel extends BaseModel {
return $siteCode . $year . $month . $day . $seqStr;
}
public function generateSpecimenID(string $orderID, int $seq): string {
return $orderID . '-S' . str_pad($seq, 2, '0', STR_PAD_LEFT);
}
public function createOrder(array $data): string {
$this->db->transStart();
try {
$orderID = $data['OrderID'] ?? $this->generateOrderID($data['SiteCode'] ?? '00');
$orderData = [
@ -70,6 +77,10 @@ class OrderTestModel extends BaseModel {
$internalOID = $this->insert($orderData);
if (!$internalOID) {
throw new \Exception('Failed to create order');
}
// Handle Order Comments
if (!empty($data['Comment'])) {
$this->db->table('ordercom')->insert([
@ -80,11 +91,15 @@ class OrderTestModel extends BaseModel {
}
// Process Tests Expansion
if (isset($data['Tests']) && is_array($data['Tests'])) {
$testToOrder = [];
$specimenConDefMap = []; // Map ConDefID to specimen info
if (isset($data['Tests']) && is_array($data['Tests'])) {
$testModel = new \App\Models\Test\TestDefSiteModel();
$grpModel = new \App\Models\Test\TestDefGrpModel();
$calModel = new \App\Models\Test\TestDefCalModel();
$testMapDetailModel = new \App\Models\Test\TestMapDetailModel();
$containerDefModel = new \App\Models\Specimen\ContainerDefModel();
foreach ($data['Tests'] as $test) {
$testSiteID = $test['TestSiteID'] ?? $test['TestID'] ?? null;
@ -93,11 +108,66 @@ class OrderTestModel extends BaseModel {
}
}
// Insert unique tests into patres
// Group tests by container requirement
$testsByContainer = [];
foreach ($testToOrder as $tid => $tinfo) {
// Find container requirement for this test
$containerReq = $this->getContainerRequirement($tid, $testMapDetailModel, $containerDefModel);
$conDefID = $containerReq['ConDefID'] ?? null;
if (!isset($testsByContainer[$conDefID])) {
$testsByContainer[$conDefID] = [
'tests' => [],
'containerInfo' => $containerReq
];
}
$testsByContainer[$conDefID]['tests'][$tid] = $tinfo;
}
// Create specimens for each unique container requirement
$specimenSeq = 1;
foreach ($testsByContainer as $conDefID => $containerData) {
$specimenID = $this->generateSpecimenID($orderID, $specimenSeq++);
$specimenData = [
'SID' => $specimenID,
'SiteID' => $data['SiteID'] ?? '1',
'OrderID' => $internalOID,
'ConDefID' => $conDefID,
'Qty' => 1,
'Unit' => 'tube',
'GenerateBy' => 'ORDER',
'CreateDate' => date('Y-m-d H:i:s')
];
$this->db->table('specimen')->insert($specimenData);
$internalSID = $this->db->insertID();
// Create specimen status
$this->db->table('specimenstatus')->insert([
'SID' => $specimenID,
'OrderID' => $internalOID,
'SpcStatus' => 'PENDING',
'CreateDate' => date('Y-m-d H:i:s')
]);
// Store mapping for patres creation
foreach ($containerData['tests'] as $tid => $tinfo) {
$specimenConDefMap[$tid] = [
'InternalSID' => $internalSID,
'SID' => $specimenID,
'ConDefID' => $conDefID
];
}
}
// Insert unique tests into patres with specimen links
if (!empty($testToOrder)) {
$resModel = new \App\Models\PatResultModel();
foreach ($testToOrder as $tid => $tinfo) {
$resModel->insert([
$specimenInfo = $specimenConDefMap[$tid] ?? null;
$patResData = [
'OrderID' => $internalOID,
'TestSiteID' => $tid,
'TestSiteCode' => $tinfo['TestSiteCode'],
@ -105,12 +175,57 @@ class OrderTestModel extends BaseModel {
'SampleID' => $orderID,
'ResultDateTime' => $orderData['TrnDate'],
'CreateDate' => date('Y-m-d H:i:s')
]);
];
if ($specimenInfo) {
$patResData['InternalSID'] = $specimenInfo['InternalSID'];
}
$resModel->insert($patResData);
}
}
}
$this->db->transComplete();
if ($this->db->transStatus() === false) {
throw new \Exception('Transaction failed');
}
return $orderID;
} catch (\Exception $e) {
$this->db->transRollback();
throw $e;
}
}
private function getContainerRequirement($testSiteID, $testMapDetailModel, $containerDefModel): array {
// Try to find container requirement from test mapping
$containerDef = $this->db->table('testmapdetail tmd')
->select('tmd.ConDefID, cd.ConCode, cd.ConName')
->join('containerdef cd', 'cd.ConDefID = tmd.ConDefID', 'left')
->where('tmd.ClientTestCode', function($builder) use ($testSiteID) {
return $builder->select('TestSiteCode')
->from('testdefsite')
->where('TestSiteID', $testSiteID);
})
->where('tmd.EndDate IS NULL')
->get()
->getRowArray();
if ($containerDef) {
return [
'ConDefID' => $containerDef['ConDefID'],
'ConCode' => $containerDef['ConCode'],
'ConName' => $containerDef['ConName']
];
}
return [
'ConDefID' => null,
'ConCode' => 'DEFAULT',
'ConName' => 'Default Container'
];
}
private function expandTest($testSiteID, &$testToOrder, $testModel, $grpModel, $calModel) {

View File

@ -17,7 +17,7 @@ class TestDefSiteModel extends BaseModel {
'Unit1', 'Factor', 'Unit2', 'Decimal',
'ReqQty', 'ReqQtyUnit', 'CollReq', 'Method', 'ExpectedTAT',
'SeqScr', 'SeqRpt', 'IndentLeft', 'FontStyle', 'VisibleScr', 'VisibleRpt',
'CountStat', 'Level',
'CountStat', 'Level', 'Requestable',
'CreateDate', 'StartDate','EndDate'
];
@ -148,7 +148,7 @@ class TestDefSiteModel extends BaseModel {
->get()->getResultArray();
$testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
$row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']);
} elseif (TestValidationService::isGroup($typeCode)) {
$row['testdefgrp'] = $db->table('testdefgrp')
@ -164,11 +164,11 @@ class TestDefSiteModel extends BaseModel {
]);
$testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
$row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']);
} elseif (TestValidationService::isTitle($typeCode)) {
$testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
$row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']);
} elseif (TestValidationService::isTechnicalTest($typeCode)) {
// Technical details are now flattened into the main row
@ -182,7 +182,7 @@ class TestDefSiteModel extends BaseModel {
}
$testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
$row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']);
}
return $row;

View File

@ -104,13 +104,4 @@ class TestMapModel extends BaseModel {
->where('testmapdetail.EndDate IS NULL')
->findAll();
}
/**
* Disable test mappings by TestSiteID
*/
public function disableByTestSiteID($testSiteID) {
$this->db->table('testmap')
->where('TestSiteID', $testSiteID)
->update(['EndDate' => date('Y-m-d H:i:s')]);
}
}

View File

@ -1045,13 +1045,34 @@ paths:
VER: Verified
REV: Reviewed
REP: Reported
- name: include
in: query
schema:
type: string
enum:
- details
description: Include specimens and tests in response
responses:
'200':
description: List of orders
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '#/components/schemas/OrderTest'
post:
tags:
- Orders
summary: Create order
summary: Create order with specimens and tests
description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation.
security:
- bearerAuth: []
requestBody:
@ -1061,12 +1082,22 @@ paths:
schema:
type: object
required:
- PatientID
- InternalPID
- Tests
properties:
PatientID:
OrderID:
type: string
VisitID:
description: Optional custom order ID (auto-generated if not provided)
InternalPID:
type: integer
description: Patient internal ID
PatVisitID:
type: integer
description: Visit ID
SiteID:
type: integer
default: 1
PlacerID:
type: string
Priority:
type: string
@ -1074,26 +1105,48 @@ paths:
- R
- S
- U
default: R
description: |
R: Routine
S: Stat
U: Urgent
SiteID:
type: integer
RequestingPhysician:
ReqApp:
type: string
description: Requesting application
Comment:
type: string
Tests:
type: array
items:
type: object
required:
- TestSiteID
properties:
TestSiteID:
type: integer
description: Test definition site ID
TestID:
type: integer
SpecimenType:
type: string
description: Alias for TestSiteID
responses:
'201':
description: Order created successfully
description: Order created successfully with specimens and tests
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
$ref: '#/components/schemas/OrderTest'
'400':
description: Validation error
'500':
description: Server error
patch:
tags:
- Orders
@ -1105,16 +1158,64 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/OrderTest'
type: object
required:
- OrderID
properties:
OrderID:
type: string
Priority:
type: string
enum:
- R
- S
- U
OrderStatus:
type: string
enum:
- ORD
- SCH
- ANA
- VER
- REV
- REP
OrderingProvider:
type: string
DepartmentID:
type: integer
WorkstationID:
type: integer
responses:
'200':
description: Order updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '#/components/schemas/OrderTest'
delete:
tags:
- Orders
summary: Delete order
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- OrderID
properties:
OrderID:
type: string
responses:
'200':
description: Order deleted
@ -1156,11 +1257,23 @@ paths:
responses:
'200':
description: Order status updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '#/components/schemas/OrderTest'
/api/ordertest/{id}:
get:
tags:
- Orders
summary: Get order by ID
description: Returns order details with associated specimens and tests
security:
- bearerAuth: []
parameters:
@ -1169,9 +1282,21 @@ paths:
required: true
schema:
type: string
description: Order ID (e.g., 0025030300001)
responses:
'200':
description: Order details
description: Order details with specimens and tests
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '#/components/schemas/OrderTest'
/api/organization/account/{id}:
get:
tags:
@ -1885,6 +2010,38 @@ paths:
security:
- bearerAuth: []
parameters:
- name: InternalPID
in: query
schema:
type: integer
description: Filter by internal patient ID (exact match)
- name: PVID
in: query
schema:
type: string
description: Filter by visit ID (partial match)
- name: PatientID
in: query
schema:
type: string
description: Filter by patient ID (partial match)
- name: PatientName
in: query
schema:
type: string
description: Search by patient name (searches in both first and last name)
- name: CreateDateFrom
in: query
schema:
type: string
format: date-time
description: Filter visits created on or after this date
- name: CreateDateTo
in: query
schema:
type: string
format: date-time
description: Filter visits created on or before this date
- name: page
in: query
schema:
@ -4899,6 +5056,9 @@ components:
SiteID:
type: integer
description: Site reference
LastLocation:
type: string
description: Full name of the last/current location from patvisitadt
CreateDate:
type: string
format: date-time
@ -5371,6 +5531,10 @@ components:
default: 1
Level:
type: integer
Requestable:
type: integer
default: 1
description: Flag indicating if test can be requested (1=yes, 0=no)
CreateDate:
type: string
format: date-time
@ -5715,13 +5879,48 @@ components:
OrderTest:
type: object
properties:
InternalOID:
type: integer
description: Internal order ID
OrderID:
type: string
PatientID:
description: Order ID (e.g., 0025030300001)
PlacerID:
type: string
VisitID:
nullable: true
InternalPID:
type: integer
description: Patient internal ID
SiteID:
type: integer
PVADTID:
type: integer
description: Visit ADT ID
ReqApp:
type: string
OrderDate:
nullable: true
Priority:
type: string
enum:
- R
- S
- U
description: |
R: Routine
S: Stat
U: Urgent
PriorityLabel:
type: string
description: Priority display text
TrnDate:
type: string
format: date-time
description: Transaction/Order date
EffDate:
type: string
format: date-time
description: Effective date
CreateDate:
type: string
format: date-time
OrderStatus:
@ -5743,23 +5942,16 @@ components:
OrderStatusLabel:
type: string
description: Order status display text
Priority:
type: string
enum:
- R
- S
- U
description: |
R: Routine
S: Stat
U: Urgent
PriorityLabel:
type: string
description: Priority display text
SiteID:
type: integer
RequestingPhysician:
type: string
Specimens:
type: array
items:
$ref: '#/components/schemas/OrderSpecimen'
description: Associated specimens for this order
Tests:
type: array
items:
$ref: '#/components/schemas/OrderTestItem'
description: Test results (patres) for this order
OrderItem:
type: object
properties:
@ -6061,6 +6253,93 @@ components:
type: string
format: date-time
description: Occupation display text
OrderSpecimen:
type: object
properties:
InternalSID:
type: integer
description: Internal specimen ID
SID:
type: string
description: Specimen ID (e.g., 0025030300001-S01)
SiteID:
type: integer
OrderID:
type: integer
description: Reference to internal order ID
ConDefID:
type: integer
description: Container Definition ID
nullable: true
ConCode:
type: string
description: Container code
nullable: true
ConName:
type: string
description: Container name
nullable: true
Qty:
type: integer
description: Quantity
Unit:
type: string
description: Unit of measurement
Status:
type: string
enum:
- PENDING
- COLLECTED
- RECEIVED
- PREPARED
- REJECTED
description: Current specimen status
GenerateBy:
type: string
description: Source that generated this specimen
CreateDate:
type: string
format: date-time
OrderTestItem:
type: object
properties:
ResultID:
type: integer
description: Unique result ID
OrderID:
type: integer
description: Reference to internal order ID
InternalSID:
type: integer
description: Reference to specimen
nullable: true
TestSiteID:
type: integer
description: Test definition site ID
TestSiteCode:
type: string
description: Test code
TestSiteName:
type: string
description: Test name
nullable: true
SID:
type: string
description: Order ID reference
SampleID:
type: string
description: Sample ID (same as OrderID)
Result:
type: string
description: Test result value
nullable: true
ResultDateTime:
type: string
format: date-time
description: Result timestamp
CreateDate:
type: string
format: date-time
TestMapDetail:
type: object
properties:

View File

@ -1,13 +1,45 @@
OrderTest:
type: object
properties:
InternalOID:
type: integer
description: Internal order ID
OrderID:
type: string
PatientID:
description: Order ID (e.g., 0025030300001)
PlacerID:
type: string
VisitID:
nullable: true
InternalPID:
type: integer
description: Patient internal ID
SiteID:
type: integer
PVADTID:
type: integer
description: Visit ADT ID
ReqApp:
type: string
OrderDate:
nullable: true
Priority:
type: string
enum: [R, S, U]
description: |
R: Routine
S: Stat
U: Urgent
PriorityLabel:
type: string
description: Priority display text
TrnDate:
type: string
format: date-time
description: Transaction/Order date
EffDate:
type: string
format: date-time
description: Effective date
CreateDate:
type: string
format: date-time
OrderStatus:
@ -23,20 +55,100 @@ OrderTest:
OrderStatusLabel:
type: string
description: Order status display text
Priority:
Specimens:
type: array
items:
$ref: '#/OrderSpecimen'
description: Associated specimens for this order
Tests:
type: array
items:
$ref: '#/OrderTestItem'
description: Test results (patres) for this order
OrderSpecimen:
type: object
properties:
InternalSID:
type: integer
description: Internal specimen ID
SID:
type: string
enum: [R, S, U]
description: |
R: Routine
S: Stat
U: Urgent
PriorityLabel:
type: string
description: Priority display text
description: Specimen ID (e.g., 0025030300001-S01)
SiteID:
type: integer
RequestingPhysician:
OrderID:
type: integer
description: Reference to internal order ID
ConDefID:
type: integer
description: Container Definition ID
nullable: true
ConCode:
type: string
description: Container code
nullable: true
ConName:
type: string
description: Container name
nullable: true
Qty:
type: integer
description: Quantity
Unit:
type: string
description: Unit of measurement
Status:
type: string
enum: [PENDING, COLLECTED, RECEIVED, PREPARED, REJECTED]
description: Current specimen status
GenerateBy:
type: string
description: Source that generated this specimen
CreateDate:
type: string
format: date-time
OrderTestItem:
type: object
properties:
ResultID:
type: integer
description: Unique result ID
OrderID:
type: integer
description: Reference to internal order ID
InternalSID:
type: integer
description: Reference to specimen
nullable: true
TestSiteID:
type: integer
description: Test definition site ID
TestSiteCode:
type: string
description: Test code
TestSiteName:
type: string
description: Test name
nullable: true
SID:
type: string
description: Order ID reference
SampleID:
type: string
description: Sample ID (same as OrderID)
Result:
type: string
description: Test result value
nullable: true
ResultDateTime:
type: string
format: date-time
description: Result timestamp
CreateDate:
type: string
format: date-time
OrderItem:
type: object

View File

@ -16,6 +16,9 @@ PatientVisit:
SiteID:
type: integer
description: Site reference
LastLocation:
type: string
description: Full name of the last/current location from patvisitadt
CreateDate:
type: string
format: date-time

View File

@ -118,6 +118,10 @@ TestDefinition:
default: 1
Level:
type: integer
Requestable:
type: integer
default: 1
description: Flag indicating if test can be requested (1=yes, 0=no)
CreateDate:
type: string
format: date-time

View File

@ -30,13 +30,33 @@
VER: Verified
REV: Reviewed
REP: Reported
- name: include
in: query
schema:
type: string
enum: [details]
description: Include specimens and tests in response
responses:
'200':
description: List of orders
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/orders.yaml#/OrderTest'
post:
tags: [Orders]
summary: Create order
summary: Create order with specimens and tests
description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation.
security:
- bearerAuth: []
requestBody:
@ -46,36 +66,68 @@
schema:
type: object
required:
- PatientID
- InternalPID
- Tests
properties:
PatientID:
OrderID:
type: string
VisitID:
description: Optional custom order ID (auto-generated if not provided)
InternalPID:
type: integer
description: Patient internal ID
PatVisitID:
type: integer
description: Visit ID
SiteID:
type: integer
default: 1
PlacerID:
type: string
Priority:
type: string
enum: [R, S, U]
default: R
description: |
R: Routine
S: Stat
U: Urgent
SiteID:
type: integer
RequestingPhysician:
ReqApp:
type: string
description: Requesting application
Comment:
type: string
Tests:
type: array
items:
type: object
required:
- TestSiteID
properties:
TestSiteID:
type: integer
description: Test definition site ID
TestID:
type: integer
SpecimenType:
type: string
description: Alias for TestSiteID
responses:
'201':
description: Order created successfully
description: Order created successfully with specimens and tests
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'
'400':
description: Validation error
'500':
description: Server error
patch:
tags: [Orders]
@ -87,16 +139,55 @@
content:
application/json:
schema:
$ref: '../components/schemas/orders.yaml#/OrderTest'
type: object
required:
- OrderID
properties:
OrderID:
type: string
Priority:
type: string
enum: [R, S, U]
OrderStatus:
type: string
enum: [ORD, SCH, ANA, VER, REV, REP]
OrderingProvider:
type: string
DepartmentID:
type: integer
WorkstationID:
type: integer
responses:
'200':
description: Order updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'
delete:
tags: [Orders]
summary: Delete order
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- OrderID
properties:
OrderID:
type: string
responses:
'200':
description: Order deleted
@ -132,11 +223,23 @@
responses:
'200':
description: Order status updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'
/api/ordertest/{id}:
get:
tags: [Orders]
summary: Get order by ID
description: Returns order details with associated specimens and tests
security:
- bearerAuth: []
parameters:
@ -145,6 +248,18 @@
required: true
schema:
type: string
description: Order ID (e.g., 0025030300001)
responses:
'200':
description: Order details
description: Order details with specimens and tests
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'

View File

@ -5,6 +5,38 @@
security:
- bearerAuth: []
parameters:
- name: InternalPID
in: query
schema:
type: integer
description: Filter by internal patient ID (exact match)
- name: PVID
in: query
schema:
type: string
description: Filter by visit ID (partial match)
- name: PatientID
in: query
schema:
type: string
description: Filter by patient ID (partial match)
- name: PatientName
in: query
schema:
type: string
description: Search by patient name (searches in both first and last name)
- name: CreateDateFrom
in: query
schema:
type: string
format: date-time
description: Filter visits created on or after this date
- name: CreateDateTo
in: query
schema:
type: string
format: date-time
description: Filter visits created on or before this date
- name: page
in: query
schema:

View File

@ -0,0 +1,210 @@
<?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()
{
$faker = Factory::create('id_ID');
// First create a patient using the same approach as PatientCreateTest
$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;
}
$patientResult = $this->withBodyFormat('json')->call('post', 'api/patient', $patientPayload);
// Check patient creation succeeded
$patientResult->assertStatus(201);
$patientBody = json_decode($patientResult->getBody(), true);
$internalPID = $patientBody['data']['InternalPID'] ?? null;
$this->assertNotNull($internalPID, 'Failed to create test patient. Response: ' . print_r($patientBody, true));
// 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($result->getBody(), true);
$this->assertIsArray($body);
$this->assertArrayHasKey('errors', $body);
}
public function testCreateOrderFailsWithInvalidPatient()
{
$payload = [
'InternalPID' => 999999,
'Tests' => [
['TestSiteID' => 1]
]
];
$result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$result->assertStatus(400);
$body = json_decode($result->getBody(), true);
$this->assertIsArray($body);
$this->assertArrayHasKey('errors', $body);
}
public function testCreateOrderWithMultipleTests()
{
$faker = Factory::create('id_ID');
// First create a patient
$patientPayload = [
"PatientID" => "ORDM" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000),
"NameFirst" => "Multi",
"NameLast" => "Test",
"Sex" => "2",
"Birthdate" => "1985-05-15",
"PatIdt" => [
"IdentifierType" => "KTP",
"Identifier" => $faker->numerify('################')
]
];
$patientResult = $this->withBodyFormat('json')->call('post', 'api/patient', $patientPayload);
$patientBody = json_decode($patientResult->getBody(), true);
$internalPID = $patientBody['data']['InternalPID'] ?? null;
$this->assertNotNull($internalPID, 'Failed to create test patient');
// 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');
}
}