Avoid coercing missing SiteID, Decimal, and age boundaries to hardcoded defaults so payload intent is retained across test creation and reference range inserts. Align patient result age checks and OpenAPI examples with day-based age bounds, with feature coverage for create variants.
315 lines
10 KiB
PHP
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 (AgeStart/AgeEnd stored in days)
|
|
if ($ref['AgeStart'] !== null || $ref['AgeEnd'] !== null) {
|
|
$birthdate = new \DateTime($patient['Birthdate']);
|
|
$today = new \DateTime();
|
|
$ageInDays = $birthdate->diff($today, true)->days;
|
|
|
|
if ($ref['AgeStart'] !== null && $ageInDays < (int) $ref['AgeStart']) {
|
|
return null;
|
|
}
|
|
if ($ref['AgeEnd'] !== null && $ageInDays > (int) $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')]);
|
|
}
|
|
}
|