clqms-be/app/Models/PatResultModel.php
root 2bcdf09b55 chore: repo-wide normalization + rules test coverage
Normalize formatting/line endings across configs, controllers, models, tests, and OpenAPI specs.

Update rule expression/rule engine implementation and remove obsolete RuleAction controller/model.

Add unit tests for rule expression syntax and multi-action behavior, and include docs updates.
2026-03-16 07:24:50 +07:00

315 lines
10 KiB
PHP

<?php
namespace App\Models;
use App\Models\RefRange\RefNumModel;
use App\Models\OrderTest\OrderTestModel;
use App\Models\Patient\PatientModel;
class PatResultModel extends BaseModel {
protected $table = 'patres';
protected $primaryKey = 'ResultID';
protected $allowedFields = [
'SiteID',
'OrderID',
'InternalSID',
'SID',
'SampleID',
'TestSiteID',
'TestSiteCode',
'AspCnt',
'Result',
'SampleType',
'ResultDateTime',
'WorkstationID',
'EquipmentID',
'RefNumID',
'RefTxtID',
'CreateDate',
'EndDate',
'ArchiveDate',
'DelDate'
];
/**
* Validate result value against reference range and return flag
*
* @param float|string $resultValue The result value to validate
* @param int $refNumID The reference range ID
* @param int $orderID The order ID to get patient info
* @return string|null 'L' for low, 'H' for high, or null for normal/no match
*/
public function validateAndFlag($resultValue, int $refNumID, int $orderID): ?string {
if (!is_numeric($resultValue)) {
return null;
}
$refNumModel = new RefNumModel();
$ref = $refNumModel->find($refNumID);
if (!$ref) {
return null;
}
// Get patient info from order
$orderModel = new OrderTestModel();
$order = $orderModel->find($orderID);
if (!$order) {
return null;
}
$patientModel = new PatientModel();
$patient = $patientModel->find($order['InternalPID']);
if (!$patient) {
return null;
}
// Check if patient matches criteria (sex)
if (!empty($ref['Sex']) && $ref['Sex'] !== 'ALL') {
if ($patient['Sex'] !== $ref['Sex']) {
return null;
}
}
// Check age criteria
if ($ref['AgeStart'] !== null || $ref['AgeEnd'] !== null) {
$birthdate = new \DateTime($patient['Birthdate']);
$today = new \DateTime();
$age = $birthdate->diff($today)->y;
if ($ref['AgeStart'] !== null && $age < $ref['AgeStart']) {
return null;
}
if ($ref['AgeEnd'] !== null && $age > $ref['AgeEnd']) {
return null;
}
}
$value = floatval($resultValue);
$low = floatval($ref['Low']);
$high = floatval($ref['High']);
// Check low
if ($ref['LowSign'] === '<=' && $value <= $low) {
return 'L';
}
if ($ref['LowSign'] === '<' && $value < $low) {
return 'L';
}
// Check high
if ($ref['HighSign'] === '>=' && $value >= $high) {
return 'H';
}
if ($ref['HighSign'] === '>' && $value > $high) {
return 'H';
}
return null; // Normal
}
/**
* Get all results for an order with test names and reference ranges
*
* @param int $orderID
* @return array
*/
public function getByOrder(int $orderID): array {
$builder = $this->db->table('patres pr');
$builder->select('
pr.ResultID,
pr.OrderID,
pr.TestSiteID,
pr.TestSiteCode,
pr.Result,
pr.ResultDateTime,
pr.RefNumID,
pr.RefTxtID,
pr.CreateDate,
tds.TestSiteName,
tds.Unit1,
tds.Unit2,
rn.Low,
rn.High,
rn.LowSign,
rn.HighSign,
rn.Display as RefDisplay
');
$builder->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left');
$builder->join('refnum rn', 'rn.RefNumID = pr.RefNumID', 'left');
$builder->where('pr.OrderID', $orderID);
$builder->where('pr.DelDate', null);
$builder->orderBy('tds.SeqScr', 'ASC');
return $builder->get()->getResultArray();
}
/**
* Get cumulative patient results across all orders
*
* @param int $internalPID
* @return array
*/
public function getByPatient(int $internalPID): array {
$builder = $this->db->table('patres pr');
$builder->select('
pr.ResultID,
pr.OrderID,
pr.TestSiteID,
pr.TestSiteCode,
pr.Result,
pr.ResultDateTime,
pr.RefNumID,
tds.TestSiteName,
tds.Unit1,
tds.Unit2,
ot.OrderID as OrderNumber,
ot.TrnDate as OrderDate
');
$builder->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left');
$builder->join('ordertest ot', 'ot.InternalOID = pr.OrderID', 'left');
$builder->where('ot.InternalPID', $internalPID);
$builder->where('pr.DelDate', null);
$builder->orderBy('pr.ResultDateTime', 'DESC');
return $builder->get()->getResultArray();
}
/**
* Update result with validation and return flag (not stored)
*
* @param int $resultID
* @param array $data
* @return array ['success' => bool, 'flag' => string|null, 'message' => string]
*/
public function updateWithValidation(int $resultID, array $data): array {
$this->db->transStart();
try {
$result = $this->find($resultID);
if (!$result) {
throw new \Exception('Result not found');
}
$flag = null;
// If result value is being updated, validate it
if (isset($data['Result']) && !empty($data['Result'])) {
$refNumID = $data['RefNumID'] ?? $result['RefNumID'];
if ($refNumID) {
$flag = $this->validateAndFlag($data['Result'], $refNumID, $result['OrderID']);
}
}
// Set update timestamp
$data['StartDate'] = date('Y-m-d H:i:s');
$updated = $this->update($resultID, $data);
if (!$updated) {
throw new \Exception('Failed to update result');
}
$this->db->transComplete();
// Fire result_updated rules (non-blocking)
try {
$fresh = $this->find($resultID);
if (is_array($fresh) && !empty($fresh['OrderID']) && !empty($fresh['TestSiteID'])) {
$orderModel = new \App\Models\OrderTest\OrderTestModel();
$order = $orderModel->find((int) $fresh['OrderID']);
$patient = null;
$age = null;
if (is_array($order) && !empty($order['InternalPID'])) {
$patientModel = new \App\Models\Patient\PatientModel();
$patient = $patientModel->find((int) $order['InternalPID']);
if (is_array($patient) && !empty($patient['Birthdate'])) {
try {
$birthdate = new \DateTime((string) $patient['Birthdate']);
$age = (new \DateTime())->diff($birthdate)->y;
} catch (\Throwable $e) {
$age = null;
}
}
}
$engine = new \App\Services\RuleEngineService();
$engine->run('result_updated', [
'order' => [
'InternalOID' => (int) ($fresh['OrderID'] ?? 0),
'Priority' => is_array($order) ? ($order['Priority'] ?? null) : null,
'TestSiteID' => (int) ($fresh['TestSiteID'] ?? 0),
],
'patient' => [
'Sex' => is_array($patient) ? ($patient['Sex'] ?? null) : null,
],
'age' => $age,
'testSiteID' => (int) ($fresh['TestSiteID'] ?? 0),
'result' => $fresh,
]);
}
} catch (\Throwable $e) {
log_message('error', 'PatResultModel::updateWithValidation rule engine error: ' . $e->getMessage());
}
return [
'success' => true,
'flag' => $flag,
'message' => 'Result updated successfully'
];
} catch (\Exception $e) {
$this->db->transRollback();
return [
'success' => false,
'flag' => null,
'message' => $e->getMessage()
];
}
}
/**
* Get single result with related data
*
* @param int $resultID
* @return array|null
*/
public function getWithRelations(int $resultID): ?array {
$builder = $this->db->table('patres pr');
$builder->select('
pr.*,
tds.TestSiteName,
tds.TestSiteCode,
tds.Unit1,
tds.Unit2,
rn.Low,
rn.High,
rn.LowSign,
rn.HighSign,
rn.Display as RefDisplay,
ot.OrderID as OrderNumber,
ot.InternalPID
');
$builder->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left');
$builder->join('refnum rn', 'rn.RefNumID = pr.RefNumID', 'left');
$builder->join('ordertest ot', 'ot.InternalOID = pr.OrderID', 'left');
$builder->where('pr.ResultID', $resultID);
$builder->where('pr.DelDate', null);
$result = $builder->get()->getRowArray();
return $result ?: null;
}
/**
* Soft delete result
*
* @param int $resultID
* @return bool
*/
public function softDelete(int $resultID): bool {
return $this->update($resultID, ['DelDate' => date('Y-m-d H:i:s')]);
}
}