clqms-be/app/Models/OrderTest/OrderTestModel.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

494 lines
18 KiB
PHP
Executable File

<?php
namespace App\Models\OrderTest;
use App\Models\BaseModel;
class OrderTestModel extends BaseModel {
protected $table = 'ordertest';
protected $primaryKey = 'InternalOID';
protected $useAutoIncrement = true;
protected $allowedFields = [
'InternalOID',
'OrderID',
'PlacerID',
'InternalPID',
'SiteID',
'PVADTID',
'ReqApp',
'Priority',
'TrnDate',
'EffDate',
'CreateDate',
'EndDate',
'ArchiveDate',
'DelDate'
];
public function generateOrderID(string $siteCode = '00'): string {
$date = new \DateTime();
$year = $date->format('y');
$month = $date->format('m');
$day = $date->format('d');
$counter = $this->db->table('counter')
->where('CounterName', 'ORDER')
->get()
->getRow();
if (!$counter) {
$this->db->table('counter')->insert([
'CounterName' => 'ORDER',
'CounterValue' => 1
]);
$seq = 1;
} else {
$seq = $counter->CounterValue + 1;
$this->db->table('counter')
->where('CounterName', 'ORDER')
->update(['CounterValue' => $seq]);
}
$seqStr = str_pad($seq, 5, '0', STR_PAD_LEFT);
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 = !empty($data['OrderID']) ? $data['OrderID'] : $this->generateOrderID($data['SiteCode'] ?? '00');
$orderData = [
'OrderID' => $orderID,
'PlacerID' => $data['PlacerID'] ?? null,
'InternalPID' => $data['InternalPID'],
'SiteID' => $data['SiteID'] ?? '1',
'PVADTID' => $data['PatVisitID'] ?? $data['PVADTID'] ?? 0,
'ReqApp' => $data['ReqApp'] ?? null,
'Priority' => $data['Priority'] ?? 'R',
'TrnDate' => $data['OrderDateTime'] ?? $data['TrnDate'] ?? date('Y-m-d H:i:s'),
'EffDate' => $data['EffDate'] ?? date('Y-m-d H:i:s'),
'CreateDate' => date('Y-m-d H:i:s')
];
$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([
'InternalOID' => $internalOID,
'Comment' => $data['Comment'],
'CreateDate' => date('Y-m-d H:i:s')
]);
}
// Process Tests Expansion
$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;
if ($testSiteID) {
$this->expandTest($testSiteID, $testToOrder, $testModel, $grpModel, $calModel);
}
}
// 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();
$patientModel = new \App\Models\Patient\PatientModel();
$patient = $patientModel->find((int) $data['InternalPID']);
$age = null;
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;
}
}
$ruleEngine = new \App\Services\RuleEngineService();
foreach ($testToOrder as $tid => $tinfo) {
$specimenInfo = $specimenConDefMap[$tid] ?? null;
$patResData = [
'OrderID' => $internalOID,
'TestSiteID' => $tid,
'TestSiteCode' => $tinfo['TestSiteCode'],
'SID' => $orderID,
'SampleID' => $orderID,
'ResultDateTime' => $orderData['TrnDate'],
'CreateDate' => date('Y-m-d H:i:s')
];
if ($specimenInfo) {
$patResData['InternalSID'] = $specimenInfo['InternalSID'];
}
$resModel->insert($patResData);
// Fire test_created rules after the test row is persisted
try {
$ruleEngine->run('test_created', [
'order' => [
'InternalOID' => $internalOID,
'Priority' => $orderData['Priority'] ?? 'R',
'TestSiteID' => $tid,
],
'patient' => [
'Sex' => is_array($patient) ? ($patient['Sex'] ?? null) : null,
],
'age' => $age,
'testSiteID' => $tid,
]);
} catch (\Throwable $e) {
log_message('error', 'OrderTestModel::createOrder rule engine error: ' . $e->getMessage());
}
}
}
}
$this->db->transComplete();
if ($this->db->transStatus() === false) {
throw new \Exception('Transaction failed');
}
return $orderID;
} catch (\Exception $e) {
$this->db->transRollback();
throw $e;
}
}
public function applyTestDelta(int $orderOID, array $testsDelta, ?array $order = null): array {
$order ??= $this->find($orderOID);
if (!$order) {
throw new \InvalidArgumentException('Order not found');
}
$summary = [
'added' => [],
'edited' => [],
'deleted' => [],
];
foreach (($testsDelta['added'] ?? []) as $item) {
if (!is_array($item)) {
throw new \InvalidArgumentException('Invalid added test payload');
}
$summary['added'][] = $this->createTestRow($order, $item);
}
foreach (($testsDelta['edited'] ?? []) as $item) {
if (!is_array($item)) {
throw new \InvalidArgumentException('Invalid edited test payload');
}
$summary['edited'][] = $this->editTestRow($orderOID, $item);
}
foreach (($testsDelta['deleted'] ?? []) as $item) {
$testSiteID = is_array($item) ? (int) ($item['TestSiteID'] ?? 0) : (int) $item;
if ($testSiteID <= 0) {
throw new \InvalidArgumentException('TestSiteID is required for deleted tests');
}
$summary['deleted'][] = $this->deleteTestRow($orderOID, $testSiteID);
}
return $summary;
}
private function createTestRow(array $order, array $item): array {
$testSiteID = (int) ($item['TestSiteID'] ?? 0);
if ($testSiteID <= 0) {
throw new \InvalidArgumentException('TestSiteID is required');
}
$testSite = $this->db->table('testdefsite')
->select('TestSiteID, TestSiteCode')
->where('TestSiteID', $testSiteID)
->where('EndDate', null)
->get()
->getRowArray();
if (!$testSite) {
throw new \InvalidArgumentException('Test site not found');
}
$payload = [
'OrderID' => (int) $order['InternalOID'],
'TestSiteID' => $testSiteID,
'TestSiteCode' => $testSite['TestSiteCode'],
'SID' => (string) ($order['OrderID'] ?? ''),
'SampleID' => (string) ($order['OrderID'] ?? ''),
'ResultDateTime' => $item['ResultDateTime'] ?? ($order['TrnDate'] ?? date('Y-m-d H:i:s')),
'CreateDate' => date('Y-m-d H:i:s'),
];
$payload['Result'] = $item['Result'] ?? $item['ResultValue'] ?? null;
$extraFields = array_intersect_key($item, array_flip([
'InternalSID',
'AspCnt',
'ResultCode',
'SampleType',
'WorkstationID',
'EquipmentID',
'RefNumID',
'RefTxtID',
]));
$payload = array_merge($payload, $extraFields);
if (!$this->db->table('patres')->insert($payload)) {
throw new \RuntimeException('Failed to create test row');
}
return [
'ResultID' => (int) $this->db->insertID(),
'TestSiteID' => $testSiteID,
];
}
private function editTestRow(int $orderOID, array $item): array {
$testSiteID = (int) ($item['TestSiteID'] ?? 0);
if ($testSiteID <= 0) {
throw new \InvalidArgumentException('TestSiteID is required');
}
$row = $this->getLatestActiveTestRow($orderOID, $testSiteID);
if (!$row) {
throw new \InvalidArgumentException('Test row not found');
}
$updateData = array_intersect_key($item, array_flip([
'InternalSID',
'AspCnt',
'Result',
'ResultCode',
'SampleType',
'ResultDateTime',
'WorkstationID',
'EquipmentID',
'RefNumID',
'RefTxtID',
]));
if (empty($updateData)) {
throw new \InvalidArgumentException('Edit payload is empty');
}
if (!$this->db->table('patres')
->where('ResultID', $row['ResultID'])
->update($updateData)) {
throw new \RuntimeException('Failed to update test row');
}
return [
'ResultID' => (int) $row['ResultID'],
'TestSiteID' => $testSiteID,
];
}
private function deleteTestRow(int $orderOID, int $testSiteID): array {
$row = $this->getLatestActiveTestRow($orderOID, $testSiteID);
if (!$row) {
throw new \InvalidArgumentException('Test row not found');
}
if (!$this->db->table('patres')
->where('ResultID', $row['ResultID'])
->update(['DelDate' => date('Y-m-d H:i:s')])) {
throw new \RuntimeException('Failed to delete test row');
}
return [
'ResultID' => (int) $row['ResultID'],
'TestSiteID' => $testSiteID,
];
}
private function getLatestActiveTestRow(int $orderOID, int $testSiteID): ?array {
$row = $this->db->table('patres')
->where('OrderID', $orderOID)
->where('TestSiteID', $testSiteID)
->where('DelDate', null)
->orderBy('ResultID', 'DESC')
->get()
->getRowArray();
return $row ?: null;
}
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) {
if (isset($testToOrder[$testSiteID])) return;
$testInfo = $testModel->find($testSiteID);
if (!$testInfo) return;
$testToOrder[$testSiteID] = [
'TestSiteCode' => $testInfo['TestSiteCode'],
'TestType' => $testInfo['TestType']
];
// Handle Group Expansion
if ($testInfo['TestType'] === 'GROUP') {
$members = $grpModel->where('TestSiteID', $testSiteID)->findAll();
foreach ($members as $m) {
$this->expandTest($m['Member'], $testToOrder, $testModel, $grpModel, $calModel);
}
}
// Handle Calculated Test Dependencies
if ($testInfo['TestType'] === 'CALC') {
$members = $grpModel->getGroupMembers($testSiteID);
foreach ($members as $member) {
$memberID = $member['TestSiteID'] ?? null;
if ($memberID) {
$this->expandTest($memberID, $testToOrder, $testModel, $grpModel, $calModel);
}
}
}
}
public function getOrder(string $orderID): ?array {
return $this->select('*')
->where('OrderID', $orderID)
->where('DelDate', null)
->get()
->getRowArray();
}
public function getOrdersByPatient(int $internalPID, ?int $pvadtid = null): array {
$builder = $this->select('*')
->where('InternalPID', $internalPID)
->where('DelDate', null);
if ($pvadtid !== null) {
$builder->where('PVADTID', $pvadtid);
}
return $builder
->orderBy('TrnDate', 'DESC')
->get()
->getResultArray();
}
public function updateStatus(string $orderID, string $status): bool {
$order = $this->getOrder($orderID);
if (!$order) return false;
return (bool)$this->db->table('orderstatus')->insert([
'InternalOID' => $order['InternalOID'],
'OrderStatus' => $status,
'CreateDate' => date('Y-m-d H:i:s')
]);
}
public function softDelete(string $orderID): bool {
return $this->where('OrderID', $orderID)->update(null, ['DelDate' => date('Y-m-d H:i:s')]);
}
}