clqms-be/app/Controllers/OrderTestController.php
mahdahar 66e9be2a04 Rename order test delta created to added
Update order test patch flow to use Tests.added instead of Tests.created across controller, model, bundled OpenAPI, and feature test coverage. Reject legacy created payloads, align validation/error text, and adjust test payloads and assertions for new added lifecycle.
2026-04-23 13:26:18 +07:00

399 lines
15 KiB
PHP
Executable File

<?php
namespace App\Controllers;
use App\Traits\ResponseTrait;
use CodeIgniter\Controller;
use App\Libraries\ValueSet;
use App\Models\OrderTest\OrderTestModel;
use App\Models\Patient\PatientModel;
use App\Models\PatVisit\PatVisitModel;
class OrderTestController extends Controller {
use ResponseTrait;
protected $db;
protected $model;
protected $patientModel;
protected $visitModel;
protected $rules;
public function __construct() {
$this->db = \Config\Database::connect();
$this->model = new OrderTestModel();
$this->patientModel = new PatientModel();
$this->visitModel = new PatVisitModel();
$this->rules = [
'InternalPID' => 'required|is_natural'
];
}
public function index() {
$internalPID = $this->request->getVar('InternalPID');
$pvadtid = $this->request->getVar('PVADTID') ?? $this->request->getVar('pvadtid');
$pvadtid = is_numeric($pvadtid) ? (int) $pvadtid : null;
$includeDetails = $this->request->getVar('include') === 'details';
try {
if ($internalPID) {
$rows = $this->model->getOrdersByPatient((int) $internalPID, $pvadtid);
} else {
$builder = $this->db->table('ordertest')
->where('DelDate', null);
if ($pvadtid !== null) {
$builder->where('PVADTID', $pvadtid);
}
$rows = $builder
->orderBy('TrnDate', 'DESC')
->get()
->getResultArray();
}
$rows = ValueSet::transformLabels($rows, [
'Priority' => 'order_priority',
'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',
'data' => $rows
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
public function show($orderID = null) {
try {
$row = $this->model->getOrder($orderID);
if (empty($row)) {
return $this->respond([
'status' => 'success',
'message' => 'Data not found.',
'data' => null
], 200);
}
$row = ValueSet::transformLabels([$row], [
'Priority' => 'order_priority',
'OrderStatus' => 'order_status',
])[0];
$row['Tests'] = $this->getOrderTestsCompact($row['InternalOID']);
return $this->respond([
'status' => 'success',
'message' => 'Data fetched successfully',
'data' => $row
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
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 getOrderTestsCompact($internalOID) {
return $this->db->table('patres pr')
->distinct()
->select('pr.TestSiteID, tds.TestSiteCode, tds.TestSiteName')
->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left')
->where('pr.OrderID', $internalOID)
->where('pr.DelDate IS NULL')
->orderBy('tds.TestSiteCode', 'ASC')
->get()
->getResultArray();
}
private function getOrderTests($internalOID) {
$tests = $this->db->table('patres pr')
->select('pr.*, tds.TestSiteCode, tds.TestSiteName, tds.TestType, tds.SeqScr AS TestSeqScr, tds.SeqRpt AS TestSeqRpt, tds.DisciplineID, d.DisciplineCode, d.DisciplineName, d.SeqScr AS DisciplineSeqScr, d.SeqRpt AS DisciplineSeqRpt')
->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left')
->join('discipline d', 'd.DisciplineID = tds.DisciplineID', 'left')
->where('pr.OrderID', $internalOID)
->where('pr.DelDate IS NULL')
->orderBy('COALESCE(d.SeqScr, 999999) ASC')
->orderBy('COALESCE(d.SeqRpt, 999999) ASC')
->orderBy('COALESCE(tds.SeqScr, 999999) ASC')
->orderBy('COALESCE(tds.SeqRpt, 999999) ASC')
->orderBy('pr.ResultID ASC')
->get()
->getResultArray();
foreach ($tests as &$test) {
$discipline = [
'DisciplineID' => $test['DisciplineID'] ?? null,
'DisciplineCode' => $test['DisciplineCode'] ?? null,
'DisciplineName' => $test['DisciplineName'] ?? null,
'SeqScr' => $test['DisciplineSeqScr'] ?? null,
'SeqRpt' => $test['DisciplineSeqRpt'] ?? null,
];
$test['Discipline'] = $discipline;
$test['SeqScr'] = $test['TestSeqScr'] ?? null;
$test['SeqRpt'] = $test['TestSeqRpt'] ?? null;
$test['DisciplineID'] = $discipline['DisciplineID'];
unset($test['DisciplineCode'], $test['DisciplineName'], $test['DisciplineSeqScr'], $test['DisciplineSeqRpt'], $test['TestSeqScr'], $test['TestSeqRpt']);
}
unset($test);
return $tests;
}
public function create() {
$input = $this->request->getJSON(true);
if (!$this->validateData($input, $this->rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
try {
if (!$this->patientModel->find($input['InternalPID'])) {
return $this->failValidationErrors(['InternalPID' => 'Patient not found']);
}
if (!empty($input['PatVisitID'])) {
$visit = $this->visitModel->find($input['PatVisitID']);
if (!$visit) {
return $this->failValidationErrors(['PatVisitID' => 'Visit not found']);
}
}
$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']);
// Rule engine triggers are fired at the test/result level (test_created, result_updated)
return $this->respondCreated([
'status' => 'success',
'message' => 'Order created successfully',
'data' => $order
], 201);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
public function update($OrderID = null) {
$input = $this->request->getJSON(true);
if ($OrderID === null || $OrderID === '') {
return $this->failValidationErrors(['OrderID' => 'OrderID is required']);
}
if (isset($input['OrderID']) && (string) $input['OrderID'] !== (string) $OrderID) {
return $this->failValidationErrors(['OrderID' => 'OrderID in URL does not match body']);
}
$transactionStarted = false;
try {
$order = $this->model->getOrder($OrderID);
if (!$order) {
return $this->failNotFound('Order not found');
}
$updateData = [];
if (array_key_exists('Priority', $input)) {
$updateData['Priority'] = $input['Priority'];
}
if (array_key_exists('OrderStatus', $input)) {
$updateData['OrderStatus'] = $input['OrderStatus'];
}
if (array_key_exists('OrderingProvider', $input)) {
$updateData['OrderingProvider'] = $input['OrderingProvider'];
}
if (array_key_exists('DepartmentID', $input)) {
$updateData['DepartmentID'] = $input['DepartmentID'];
}
if (array_key_exists('WorkstationID', $input)) {
$updateData['WorkstationID'] = $input['WorkstationID'];
}
$testsDelta = null;
if (array_key_exists('Tests', $input)) {
$testsDelta = $this->normalizeTestsDelta($input['Tests']);
}
if (empty($updateData) && $testsDelta === null) {
return $this->failValidationErrors(['error' => 'Update payload is required']);
}
$hasUpdates = !empty($updateData) || $testsDelta !== null;
if ($hasUpdates) {
$this->db->transStart();
$transactionStarted = true;
if (!empty($updateData)) {
$updated = $this->model->update($order['InternalOID'], $updateData);
if (!$updated) {
throw new \RuntimeException('Failed to update order');
}
}
if ($testsDelta !== null) {
$this->model->applyTestDelta((int) $order['InternalOID'], $testsDelta, $order);
}
$this->db->transComplete();
if ($this->db->transStatus() === false) {
throw new \RuntimeException('Transaction failed');
}
$transactionStarted = false;
}
$updatedOrder = $this->model->getOrder($OrderID);
$updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']);
$updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']);
return $this->respond([
'status' => 'success',
'message' => 'Order updated successfully',
'data' => $updatedOrder
], 200);
} catch (\InvalidArgumentException $e) {
if ($transactionStarted) {
$this->db->transRollback();
}
return $this->failValidationErrors(['Tests' => $e->getMessage()]);
} catch (\Exception $e) {
if ($transactionStarted) {
$this->db->transRollback();
}
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
private function normalizeTestsDelta($tests): ?array {
if (!is_array($tests)) {
throw new \InvalidArgumentException('Tests must be an object');
}
if (array_key_exists('created', $tests)) {
throw new \InvalidArgumentException('created tests are not supported');
}
$delta = [
'added' => [],
'edited' => [],
'deleted' => [],
];
foreach (array_keys($delta) as $key) {
if (!array_key_exists($key, $tests)) {
continue;
}
if (!is_array($tests[$key])) {
throw new \InvalidArgumentException(ucfirst($key) . ' tests must be an array');
}
$delta[$key] = $tests[$key];
}
if (empty($delta['added']) && empty($delta['edited']) && empty($delta['deleted'])) {
throw new \InvalidArgumentException('Tests delta is required');
}
return $delta;
}
public function delete() {
$input = $this->request->getJSON(true);
$orderID = $input['OrderID'] ?? null;
if (empty($orderID)) {
return $this->failValidationErrors(['OrderID' => 'OrderID is required']);
}
try {
$order = $this->model->getOrder($orderID);
if (!$order) {
return $this->failNotFound('Order not found');
}
$this->model->softDelete($orderID);
return $this->respondDeleted([
'status' => 'success',
'message' => 'Order deleted successfully'
]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
public function updateStatus() {
$input = $this->request->getJSON(true);
if (empty($input['OrderID']) || empty($input['OrderStatus'])) {
return $this->failValidationErrors(['error' => 'OrderID and OrderStatus are required']);
}
$validStatuses = ['ORD', 'SCH', 'ANA', 'VER', 'REV', 'REP'];
if (!in_array($input['OrderStatus'], $validStatuses)) {
return $this->failValidationErrors(['OrderStatus' => 'Invalid status. Valid: ' . implode(', ', $validStatuses)]);
}
try {
$order = $this->model->getOrder($input['OrderID']);
if (!$order) {
return $this->failNotFound('Order not found');
}
$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' => $updatedOrder
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
}