You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1971 lines
75 KiB
1971 lines
75 KiB
<?php |
|
|
|
/** |
|
* Functions to support parsing and saving hl7 results. |
|
* |
|
* Copyright (C) 2013-2016 Rod Roark <rod@sunsetsystems.com> |
|
* Copyright (C) 2017-2020 Jerry Padgett <sjpadgett@gmail.com> |
|
* |
|
* LICENSE: This program is free software; you can redistribute it and/or |
|
* modify it under the terms of the GNU General Public License |
|
* as published by the Free Software Foundation; either version 2 |
|
* of the License, or (at your option) any later version. |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* You should have received a copy of the GNU General Public License |
|
* along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>. |
|
* |
|
* @package OpenEMR |
|
* @author Rod Roark <rod@sunsetsystems.com> |
|
* @author Jerry Padgett <sjpadgett@gmail.com> |
|
* 07-2015: Ensoftek: Edited for MU2 170.314(b)(5)(A) |
|
*/ |
|
|
|
require_once($GLOBALS['srcdir'] . "/forms.inc"); |
|
require_once($GLOBALS['srcdir'] . "/pnotes.inc"); |
|
|
|
use OpenEMR\Common\Crypto\CryptoGen; |
|
use OpenEMR\Common\Logging\EventAuditLogger; |
|
use phpseclib\Net\SFTP; |
|
|
|
$rhl7_return = array(); |
|
|
|
function parseZPS($segment) |
|
{ |
|
$composites = $segment; //explode('|', $segment); |
|
|
|
// Try to parse composites |
|
foreach ($composites as $key => $composite) { |
|
// If it is a composite ... |
|
if (!(strpos($composite, '^') === false)) { |
|
$composites[$key] = explode('^', $composite); |
|
} |
|
} |
|
|
|
// Find out where we are |
|
$pos = 0; |
|
|
|
[ |
|
$__garbage, // Skip index [0], it's the type |
|
$zps[$pos]['set_id'], |
|
$zps[$pos]['lab_id'], |
|
$zps[$pos]['lab_name'], |
|
$zps[$pos]['lab_address'], |
|
$zps[$pos]['lab_phone'], |
|
$__garbage, |
|
$zps[$pos]['lab_director'], |
|
$__garbage, |
|
$zps[$pos]['lab_clia'] |
|
] = $composites; |
|
$labdir = $zps[0]['lab_director'][0] . " " . $zps[0]['lab_director'][1] . ", " . $zps[0]['lab_director'][2]; |
|
$address = $zps[0]['lab_address'][0] . "\n" . $zps[0]['lab_address'][2] . " " . $zps[0]['lab_address'][3] . |
|
" " . $zps[0]['lab_address'][4]; |
|
$r = $zps[0]['lab_name'] . "\n" . $address . "\n" . $zps[0]['lab_phone'] . "\n" . $labdir . "\n"; |
|
return $r; |
|
} |
|
|
|
function rhl7LogMsg($msg, $fatal = true) |
|
{ |
|
global $rhl7_return; |
|
if ($fatal) { |
|
$rhl7_return['mssgs'][] = '*' . $msg; |
|
$rhl7_return['fatal'] = true; |
|
EventAuditLogger::instance()->newEvent( |
|
"lab-results-error", |
|
$_SESSION['authUser'], |
|
$_SESSION['authProvider'], |
|
0, |
|
$msg |
|
); |
|
} else { |
|
$rhl7_return['mssgs'][] = '>' . $msg; |
|
} |
|
|
|
return $rhl7_return; |
|
} |
|
|
|
function rhl7InsertRow(&$arr, $tablename) |
|
{ |
|
if (empty($arr)) { |
|
return; |
|
} |
|
|
|
// echo "<!-- "; // debugging |
|
// print_r($arr); |
|
// echo " -->\n"; |
|
|
|
$query = "INSERT INTO $tablename SET"; |
|
$binds = array(); |
|
$sep = ''; |
|
foreach ($arr as $key => $value) { |
|
$query .= "$sep `$key` = ?"; |
|
$sep = ','; |
|
$binds[] = $value; |
|
} |
|
|
|
$arr = array(); |
|
return sqlInsert($query, $binds); |
|
} |
|
|
|
// Write all of the accumulated reports and their results. |
|
function rhl7FlushMain(&$amain, $commentdelim = "\n") |
|
{ |
|
foreach ($amain as $arr) { |
|
if (!isset($amain[0]['rep']['procedure_order_id'])) { |
|
if (!isset($arr['rep']['procedure_order_id'])) { |
|
continue; |
|
} elseif (isset($amain[0]['rep']['report_notes'])) { |
|
$arr['rep']['report_notes'] .= $amain[0]['rep']['report_notes']; |
|
unset($amain[0]); |
|
} |
|
} |
|
$procedure_report_id = rhl7InsertRow($arr['rep'], 'procedure_report'); |
|
foreach ($arr['res'] as $ares) { |
|
$ares['procedure_report_id'] = $procedure_report_id; |
|
// obxkey was used to identify parent results but is not stored. |
|
unset($ares['obxkey']); |
|
// If TX result is not over 10 characters, move it from comments to result field. |
|
if ($ares['result'] === '' && $ares['result_data_type'] == 'L') { |
|
$i = strpos($ares['comments'], $commentdelim); |
|
if ($i && $i <= 10) { |
|
$ares['result'] = substr($ares['comments'], 0, $i); |
|
$ares['comments'] = substr($ares['comments'], $i); |
|
} |
|
} |
|
|
|
rhl7InsertRow($ares, 'procedure_result'); |
|
} |
|
} |
|
} |
|
|
|
// Write the MDM document if appropriate. |
|
// |
|
function rhl7FlushMDM($patient_id, $mdm_docname, $mdm_datetime, $mdm_text, $mdm_category_id, $provider) |
|
{ |
|
if ($patient_id) { |
|
if (!empty($mdm_docname)) { |
|
$mdm_docname .= '_'; |
|
} |
|
|
|
$mdm_docname .= preg_replace('/[^0-9]/', '', $mdm_datetime); |
|
$filename = $mdm_docname . '.txt'; |
|
$d = new Document(); |
|
$rc = $d->createDocument($patient_id, $mdm_category_id, $filename, 'text/plain', $mdm_text); |
|
if (!$rc) { |
|
rhl7LogMsg(xl('Document created') . ": $filename", false); |
|
if ($provider) { |
|
$d->postPatientNote($provider, $mdm_category_id, xl('Electronic document received')); |
|
rhl7LogMsg(xl('Notification sent to') . ": $provider", false); |
|
} else { |
|
rhl7LogMsg(xl('No provider was matched'), false); |
|
} |
|
} |
|
|
|
return $rc; |
|
} |
|
|
|
return ''; |
|
} |
|
|
|
function rhl7Text($s, $allow_newlines = false) |
|
{ |
|
$s = str_replace('\\S\\', '^', $s); |
|
$s = str_replace('\\F\\', '|', $s); |
|
$s = str_replace('\\R\\', '~', $s); |
|
$s = str_replace('\\T\\', '&', $s); |
|
$s = str_replace('\\X0d\\', "\r", $s); |
|
$s = str_replace('\\E\\', '\\', $s); |
|
if ($allow_newlines) { |
|
$s = str_replace('\\.br\\', "\n", $s); |
|
} else { |
|
$s = str_replace('\\.br\\', '~', $s); |
|
} |
|
|
|
return $s; |
|
} |
|
|
|
function rhl7DateTime($s) |
|
{ |
|
// Remove UTC offset if present. |
|
if (preg_match('/^([0-9.]+)[+-]/', $s, $tmp)) { |
|
$s = $tmp[1]; |
|
} |
|
|
|
$s = preg_replace('/[^0-9]/', '', $s); |
|
if (empty($s)) { |
|
return '0000-00-00 00:00:00'; |
|
} |
|
|
|
$ret = substr($s, 0, 4) . '-' . substr($s, 4, 2) . '-' . substr($s, 6, 2); |
|
if (strlen($s) > 8) { |
|
$ret .= ' ' . substr($s, 8, 2) . ':' . substr($s, 10, 2) . ':'; |
|
if (strlen($s) > 12) { |
|
$ret .= substr($s, 12, 2); |
|
} else { |
|
$ret .= '00'; |
|
} |
|
} |
|
|
|
return $ret; |
|
} |
|
|
|
function rhl7DateTimeZone($s) |
|
{ |
|
// UTC offset if present always begins with "+" or "-". |
|
if (preg_match('/^[0-9.]+([+-].*)$/', $s, $tmp)) { |
|
return trim($tmp[1]); |
|
} |
|
|
|
return ''; |
|
} |
|
|
|
function rhl7Date($s) |
|
{ |
|
return substr(rhl7DateTime($s), 0, 10); |
|
} |
|
|
|
function rhl7Abnormal($s) |
|
{ |
|
if ($s == '') { |
|
return 'no'; |
|
} |
|
|
|
if ($s == 'N') { |
|
return 'no'; |
|
} |
|
|
|
if ($s == 'A') { |
|
return 'yes'; |
|
} |
|
|
|
if ($s == 'H') { |
|
return 'high'; |
|
} |
|
|
|
if ($s == 'L') { |
|
return 'low'; |
|
} |
|
|
|
if ($s == 'HH') { |
|
return 'vhigh'; |
|
} |
|
|
|
if ($s == 'LL') { |
|
return 'vlow'; |
|
} |
|
|
|
return rhl7Text($s); |
|
} |
|
|
|
function rhl7ReportStatus($s) |
|
{ |
|
if ($s == 'F') { |
|
return 'final'; |
|
} |
|
|
|
if ($s == 'P') { |
|
return 'prelim'; |
|
} |
|
|
|
if ($s == 'C') { |
|
return 'correct'; |
|
} |
|
|
|
if ($s == 'X') { |
|
return 'error'; |
|
} |
|
|
|
return rhl7Text($s); |
|
} |
|
|
|
/** |
|
* Convert a lower case file extension to a MIME type. |
|
* The extension comes from OBX[5][0] which is itself a huge assumption that |
|
* the HL7 2.3 standard does not help with. Don't be surprised when we have to |
|
* adapt to conventions of various other labs. |
|
* |
|
* @param string $fileext The lower case extension. |
|
* @return string MIME type. |
|
*/ |
|
function rhl7MimeType($fileext) |
|
{ |
|
if ($fileext == 'pdf') { |
|
return 'application/pdf'; |
|
} |
|
|
|
if ($fileext == 'doc') { |
|
return 'application/msword'; |
|
} |
|
|
|
if ($fileext == 'rtf') { |
|
return 'application/rtf'; |
|
} |
|
|
|
if ($fileext == 'txt') { |
|
return 'text/plain'; |
|
} |
|
|
|
if ($fileext == 'zip') { |
|
return 'application/zip'; |
|
} |
|
|
|
return 'application/octet-stream'; |
|
} |
|
|
|
/** |
|
* Extract encapsulated document data according to its encoding type. |
|
* |
|
* @param string $enctype Encoding type from OBX[5][3]. |
|
* @param string &$src Encoded data from OBX[5][4]. |
|
* @return string Decoded data, or FALSE if error. |
|
*/ |
|
function rhl7DecodeData($enctype, &$src) |
|
{ |
|
if ($enctype == 'Base64') { |
|
return base64_decode($src); |
|
} |
|
|
|
if ($enctype == 'A') { |
|
return rhl7Text($src); |
|
} |
|
|
|
if ($enctype == 'Hex') { |
|
$data = ''; |
|
for ($i = 0; $i < strlen($src) - 1; $i += 2) { |
|
$data .= chr(hexdec($src[$i] . $src[$i + 1])); |
|
} |
|
|
|
return $data; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
function rhl7CWE($s, $componentdelimiter) |
|
{ |
|
$out = ''; |
|
if ($s === '') { |
|
return $out; |
|
} |
|
|
|
$arr = explode($componentdelimiter, $s); |
|
if (!empty($arr[8])) { |
|
$out = $arr[8]; |
|
} else { |
|
$out = $arr[0]; |
|
if (isset($arr[1])) { |
|
$out .= " (" . $arr[1] . ")"; |
|
} |
|
} |
|
|
|
return $out; |
|
} |
|
|
|
/** |
|
* Parse the SPM segment and get the specimen display name and update the table. |
|
* |
|
* @param string $specimen Encoding type from SPM. |
|
*/ |
|
function rhl7UpdateReportWithSpecimen(&$amain, $specimen, $d2) |
|
{ |
|
$specimen_display = ''; |
|
|
|
// SPM4: Specimen Type: Example: 119297000^BLD^SCT^BldSpc^Blood^99USA^^^Blood Specimen |
|
$specimen_display = rhl7CWE($specimen[4], $d2); |
|
|
|
$tmpnotes = xl('Specimen type') . ': ' . $specimen_display; |
|
$tmp = rhl7CWE($specimen[21], $d2); |
|
if ($tmp) { |
|
$tmpnotes .= '; ' . xl('Rejected') . ': ' . $tmp; |
|
} |
|
|
|
$tmp = rhl7CWE($specimen[24], $d2); |
|
if ($tmp) { |
|
$tmpnotes .= '; ' . xl('Condition') . ': ' . $tmp; |
|
} |
|
|
|
$alast = count($amain) - 1; |
|
$amain[$alast]['rep']['specimen_num'] = $specimen_display; |
|
$amain[$alast]['rep']['report_notes'] .= rhl7Text($tmpnotes) . "\n"; |
|
} |
|
|
|
/** |
|
* Get the Performing Lab Details from the OBX segment. Mandatory for MU2. |
|
* |
|
* @param string $obx23 Encoding type from OBX23. |
|
* @param string $obx23 Encoding type from OBX24. |
|
* @param string $obx23 Encoding type from OBX25. |
|
* @param string $obx23 New line character. |
|
*/ |
|
function getPerformingOrganizationDetails($obx23, $obx24, $obx25, $componentdelimiter, $commentdelim) |
|
{ |
|
$s = null; |
|
|
|
if (!empty($obx23) || !empty($obx24) || !empty($obx25)) { |
|
// Organization Name |
|
// OBX23 Example: "Century Hospital^^^^^NIST-AA-1&2.16.840.1.113883.3.72.5.30.1&ISO^XX^^^987" |
|
$obx23_segs = explode($componentdelimiter, $obx23); |
|
if (!empty($obx23_segs[0])) { |
|
$s .= $obx23_segs[0] . $commentdelim; |
|
} |
|
|
|
// Medical Director |
|
// OBX25 Example: "2343242^Knowsalot^Phil^J.^III^Dr.^^^NIST-AA-1&2.16.840.1.113883.3.72.5.30.1&ISO^L^^^DNSPM" |
|
// Dr. Phil Knowsalot J. III |
|
if (!empty($obx25)) { |
|
$obx25_segs = explode($componentdelimiter, $obx25); |
|
$s .= "$obx25_segs[5] $obx25_segs[2] $obx25_segs[1] $obx25_segs[3] $obx25_segs[4]" . $commentdelim; |
|
} |
|
|
|
// Organization Address |
|
// OBX24 Example: "2070 Test Park^^Los Angeles^CA^90067^USA^B^^06037" |
|
if (!empty($obx24)) { |
|
$obx24 = str_replace('~', ' ', $obx24); |
|
$obx24_segs = explode($componentdelimiter, $obx24); |
|
$s .= "$obx24_segs[0]$commentdelim$obx24_segs[1]$commentdelim$obx24_segs[2], " . |
|
"$obx24_segs[3] $obx24_segs[4]$commentdelim$obx24_segs[5]$commentdelim"; |
|
if (!empty($obx24_segs[6])) { |
|
$s .= "$obx24_segs[6]$commentdelim"; |
|
} |
|
if (!empty($obx24_segs[8])) { |
|
$s .= "County/Parish Code: $obx24_segs[8]$commentdelim"; |
|
} |
|
} |
|
} |
|
|
|
return $s; |
|
} |
|
|
|
/** |
|
* Look for a patient matching the given data. |
|
* Return values are: |
|
* >0 Definite match, this is the pid. |
|
* 0 No patient is close to a match. |
|
* -1 It's not clear if there is a match. |
|
*/ |
|
function match_patient($ptarr) |
|
{ |
|
$in_ss = str_replace('-', '', $ptarr['ss']); |
|
$in_fname = $ptarr['fname']; |
|
$in_lname = $ptarr['lname']; |
|
$in_dob = $ptarr['DOB']; |
|
$in_sex = strtoupper($ptarr['sex']) == 'M' ? 'Male' : 'Female'; // AND sex IS NOT NULL AND sex = ? |
|
|
|
$patient_id = 0; |
|
$res = sqlStatement( |
|
"SELECT pid FROM patient_data WHERE " . |
|
"((ss IS NULL OR ss = '' OR '' = ?) AND " . |
|
"fname IS NOT NULL AND fname != '' AND fname = ? AND " . |
|
"lname IS NOT NULL AND lname != '' AND lname = ? AND " . |
|
"DOB IS NOT NULL AND DOB = ?) OR " . |
|
"(ss IS NOT NULL AND ss != '' AND REPLACE(ss, '-', '') = ? AND (" . |
|
"fname IS NOT NULL AND fname != '' AND fname = ? OR " . |
|
"lname IS NOT NULL AND lname != '' AND lname = ? OR " . |
|
"DOB IS NOT NULL AND DOB = ?)) " . |
|
"ORDER BY ss DESC, pid DESC LIMIT 2", |
|
array($in_ss, $in_fname, $in_lname, $in_dob, $in_ss, $in_fname, $in_lname, $in_dob) |
|
); |
|
if (sqlNumRows($res) > 1) { |
|
// Multiple matches, so ambiguous. |
|
$patient_id = -1; |
|
} elseif (sqlNumRows($res) == 1) { |
|
// Got exactly one match, so use it. |
|
$tmp = sqlFetchArray($res); |
|
$patient_id = intval($tmp['pid']); |
|
} else { |
|
// No match good enough, figure out if there's enough ambiguity to ask the user. |
|
$tmp = sqlQuery( |
|
"SELECT pid FROM patient_data WHERE " . |
|
"(ss IS NOT NULL AND ss != '' AND REPLACE(ss, '-', '') = ?) OR " . |
|
"(fname IS NOT NULL AND fname != '' AND fname = ? AND " . |
|
"lname IS NOT NULL AND lname != '' AND lname = ?) OR " . |
|
"(DOB IS NOT NULL AND DOB = ?) " . |
|
"LIMIT 1", |
|
array($in_ss, $in_fname, $in_lname, $in_dob) |
|
); |
|
if (!empty($tmp['pid'])) { |
|
$patient_id = -1; |
|
} |
|
} |
|
|
|
return $patient_id; |
|
} |
|
|
|
/** |
|
* Look for a lab matching the given XCN field from some segment. |
|
* |
|
* @param array $seg MSH seg identifying a provider. |
|
* @return mixed TRUE, or FALSE if no match. |
|
*/ |
|
function match_lab(&$hl7, $send_acct, $lab_acct = '', $lab_app = '', $lab_npi = '') |
|
{ |
|
if (empty($hl7)) { |
|
return false; |
|
} |
|
$d0 = "\r"; |
|
$d1 = substr($hl7, 3, 1); // typically | |
|
|
|
$segs = explode($d0, $hl7); |
|
$a = explode($d1, $segs[0]); |
|
if ($a[0] != 'MSH') { |
|
unset($segs); |
|
return false; |
|
} |
|
|
|
unset($segs); |
|
// CMS has deactivated NPI 1891752424, not sure who AMMON is |
|
if ($lab_npi == '1891752424' || strtoupper($lab_npi) == 'AMMON') { |
|
if (strtoupper(trim($a[5])) == strtoupper(trim($send_acct))) { |
|
$srch = '|' . strtoupper(trim($send_acct)) . '-'; |
|
$hl7 = str_replace($srch, "|", $hl7); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
if ( |
|
strtoupper(trim($a[5])) == strtoupper(trim($send_acct)) || |
|
strtoupper(trim($a[3])) == strtoupper(trim($lab_acct)) || |
|
strtoupper(trim($a[2])) == strtoupper(trim($lab_app)) |
|
) { |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// lookup code from configuration to validate codes after order submit ie manual/batch |
|
function lookupTestCode($labid, $procedure_code) |
|
{ |
|
|
|
$query = "SELECT procedure_type_id, procedure_code, procedure_type, name, transport " . |
|
"FROM procedure_type WHERE " . |
|
"lab_id = ? AND " . |
|
"(procedure_type LIKE 'ord' OR procedure_type LIKE 'pro') AND " . |
|
"activity = 1 AND procedure_code = ? " . |
|
"LIMIT 1"; |
|
$res = sqlQuery($query, array($labid, $procedure_code)); |
|
|
|
return $res; |
|
} |
|
|
|
// create encounter |
|
function create_encounter($pid, $provider_id, $order_date, $lab_name) |
|
{ |
|
global $orphanLog; |
|
$conn = $GLOBALS['adodb']['db']; |
|
$encounter = $conn->GenID("sequences"); |
|
addForm( |
|
$encounter, |
|
"Auto Generated Lab Encounter", |
|
sqlInsert( |
|
"INSERT INTO form_encounter SET " . |
|
"date = ?, " . |
|
"onset_date = '', " . |
|
"reason = ?, " . |
|
"sensitivity = 'normal', " . |
|
"referral_source = '', " . |
|
"pid = ?, " . |
|
"encounter = ?, " . |
|
"provider_id = ?", |
|
array( |
|
date('Y-m-d H:i:s', strtotime($order_date)), |
|
"Generated encounter for " . strtoupper($lab_name) . " result", |
|
$pid, |
|
$encounter, |
|
($provider_id ?? '') |
|
) |
|
), |
|
"newpatient", |
|
$pid, |
|
0, |
|
date('Y-m-d'), |
|
'SYSTEM' |
|
); |
|
$orphanLog .= "New Encounter: $encounter created. "; |
|
return $encounter ?: 0; |
|
} |
|
|
|
// send a message (pnote) to provider to inform about orders creation |
|
function labNotice($pid, $newtext, $assigned_to = 'admin', $datetime = '', $labname = '') |
|
{ |
|
if ($pid > 999999990) { |
|
return; |
|
} |
|
|
|
$message_sender = $_SESSION['authUser']; |
|
$message_group = 'Default'; |
|
$authorized = '0'; |
|
$activity = '1'; |
|
$title = 'Lab Results'; |
|
$message_status = 'New'; |
|
if (empty($datetime)) { |
|
$datetime = date('Y-m-d H:i:s'); |
|
} |
|
|
|
if (!$assigned_to) { |
|
$assigned_to = $_SESSION['authUser']; |
|
} |
|
$notify = $assigned_to; //@todo get user lookup |
|
|
|
$body = date('Y-m-d H:i') . ' (' . $labname . ' to ' . $notify . ') ' . $newtext; |
|
|
|
return sqlInsert( |
|
"INSERT INTO pnotes (date, body, pid, user, groupname, " . |
|
"authorized, activity, title, assigned_to, message_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |
|
array( |
|
$datetime, |
|
$body, |
|
$pid, |
|
$message_sender, |
|
$message_group, |
|
$authorized, |
|
$activity, |
|
$title, |
|
$notify, |
|
$message_status |
|
) |
|
); |
|
} |
|
|
|
/** |
|
* Look for a local provider matching the given XCN field from some segment. |
|
* |
|
* @param array $arr array(NPI, lastname, firstname) identifying a provider. |
|
* @return mixed Array(id, username), or FALSE if no match. |
|
*/ |
|
function match_provider($arr) |
|
{ |
|
if (empty($arr)) { |
|
return false; |
|
} |
|
|
|
$op_lname = $op_fname = ''; |
|
$op_npi = preg_replace('/[^0-9]/', '', $arr[0]); |
|
if (!empty($arr[1])) { |
|
$op_lname = $arr[1]; |
|
} |
|
|
|
if (!empty($arr[2])) { |
|
$op_fname = $arr[2]; |
|
} |
|
|
|
if ($op_npi || ($op_fname && $op_lname)) { |
|
if ($op_npi) { |
|
if ($op_fname && $op_lname) { |
|
$where = "((npi IS NOT NULL AND npi = ?) OR ((npi IS NULL OR npi = ?) AND lname = ? AND fname = ?))"; |
|
$qarr = array($op_npi, '', $op_lname, $op_fname); |
|
} else { |
|
$where = "npi IS NOT NULL AND npi = ?"; |
|
$qarr = array($op_npi); |
|
} |
|
} else { |
|
$where = "lname = ? AND fname = ?"; |
|
$qarr = array($op_lname, $op_fname); |
|
} |
|
|
|
$oprow = sqlQuery( |
|
"SELECT id, username FROM users WHERE " . |
|
"username IS NOT NULL AND username != '' AND $where " . |
|
"ORDER BY active DESC, authorized DESC, username, id LIMIT 1", |
|
$qarr |
|
); |
|
if (!empty($oprow)) { |
|
return $oprow; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
function ucname($string) |
|
{ |
|
$string = ucwords(strtolower($string)); |
|
|
|
foreach (array('-', '\'') as $delimiter) { |
|
if (strpos($string, $delimiter) !== false) { |
|
$string = implode($delimiter, array_map('ucfirst', explode($delimiter, $string))); |
|
} |
|
} |
|
return $string; |
|
} |
|
|
|
/** |
|
* Create a patient using whatever patient_data attributes are provided. |
|
*/ |
|
function create_skeleton_patient($patient_data) |
|
{ |
|
global $orphanLog; |
|
$employer_data = array(); |
|
$tmp = sqlQuery("SELECT MAX(pid)+1 AS pid FROM patient_data"); |
|
$ptid = empty($tmp['pid']) ? 1 : intval($tmp['pid']); |
|
if (!isset($patient_data['pubpid'])) { |
|
$patient_data['pubpid'] = $ptid; |
|
} |
|
|
|
updatePatientData($ptid, $patient_data, true); |
|
updateEmployerData($ptid, $employer_data, true); |
|
newHistoryData($ptid); |
|
$tmp = "Pid: $ptid " . $patient_data['fname'] . ' ' . $patient_data['lname'] . ' ' . $patient_data['DOB']; |
|
$orphanLog .= "New Patient for $tmp created. "; |
|
return $ptid; |
|
} |
|
|
|
/** |
|
* Parse and save. |
|
* |
|
* @param string &$hl7 The input HL7 text |
|
* @param string &$matchreq Array of shared patient matching requests |
|
* @param int $lab_id Lab ID |
|
* @param char $direction B=Bidirectional, R=Results-only |
|
* @param bool $dryrun True = do not update anything, just report errors |
|
* @param array $matchresp Array of responses to match requests; key is relative segment number, |
|
* value is an existing pid or 0 to specify creating a patient |
|
* @return array Array of errors and match requests, if any |
|
*/ |
|
function receive_hl7_results(&$hl7, &$matchreq, $lab_id = 0, $direction = 'B', $dryrun = false, $matchresp = null) |
|
{ |
|
global $rhl7_return; |
|
global $orphanLog; |
|
global $lab_npi; |
|
//$direction = 'R'; |
|
// This will hold returned error messages and related variables. |
|
$rhl7_return = array(); |
|
$rhl7_return['mssgs'] = array(); |
|
$rhl7_return['needmatch'] = false; // indicates if this file is pending a match request |
|
|
|
$rhl7_segnum = 0; |
|
$obrPerformingOrganization = ''; |
|
|
|
if (substr($hl7, 0, 3) != 'MSH') { |
|
return rhl7LogMsg(xl('Input does not begin with a MSH segment'), true); |
|
} |
|
|
|
// This array holds everything to be written to the database. |
|
// We save and postpone these writes in case of errors while processing the message, |
|
// so we can look up data from parent results when child results are encountered, |
|
// and for other logic simplification. |
|
// Each element of this array is another array containing the following possible keys: |
|
// 'rep' - row of data to write to procedure_report |
|
// 'res' - array of rows to write to procedure_result for this procedure_report |
|
// 'fid' - unique lab-provided identifier for this report |
|
// |
|
$amain = array(); |
|
|
|
// End-of-line delimiter for text in procedure_result.comments and other multi-line notes. |
|
$commentdelim = "\n"; |
|
|
|
// Ensoftek: Different labs seem to send different EOLs. Edit HL7 input to a character we know. |
|
$hl7 = (string)str_replace(array("\r\n", "\r", "\n"), "\r", $hl7); |
|
|
|
$today = time(); |
|
$in_message_lab_name = ''; |
|
$in_message_id = ''; |
|
$in_ssn = ''; |
|
$in_dob = ''; |
|
$in_lname = ''; |
|
$in_fname = ''; |
|
$in_orderid = 0; |
|
$in_procedure_code = ''; |
|
$in_report_status = ''; |
|
$in_encounter = 0; |
|
$patient_id = 0; // for results-only patient matching logic |
|
$porow = false; |
|
$pcrow = false; |
|
$oprow = false; |
|
|
|
$code_seq_array = array(); // tracks sequence numbers of order codes |
|
$results_category_id = 0; // document category ID for lab results |
|
|
|
// This is so we know where we are if a segment like NTE that can appear in |
|
// different places is encountered. |
|
$context = ''; |
|
|
|
// This will be "ORU" or "MDM". |
|
$msgtype = ''; |
|
|
|
// Stuff collected for MDM documents. |
|
$mdm_datetime = ''; |
|
$mdm_docname = ''; |
|
$mdm_text = ''; |
|
|
|
// Delimiters |
|
$d0 = "\r"; |
|
$d1 = substr($hl7, 3, 1); // typically | |
|
$d2 = substr($hl7, 4, 1); // typically ^ |
|
$d3 = substr($hl7, 5, 1); // typically ~ |
|
$d4 = substr($hl7, 6, 1); // typically \ |
|
$d5 = substr($hl7, 7, 1); // typically & |
|
|
|
// We'll need the document category IDs for any embedded documents. |
|
$catrow = sqlQuery( |
|
"SELECT id FROM categories WHERE name = ?", |
|
array($GLOBALS['lab_results_category_name']) |
|
); |
|
if (empty($catrow['id'])) { |
|
return rhl7LogMsg(xl('Document category for lab results does not exist') . |
|
': ' . $GLOBALS['lab_results_category_name'], true); |
|
} else { |
|
$results_category_id = $catrow['id']; |
|
$mdm_category_id = $results_category_id; |
|
$catrow = sqlQuery( |
|
"SELECT id FROM categories WHERE name = ?", |
|
array($GLOBALS['gbl_mdm_category_name']) |
|
); |
|
if (!empty($catrow['id'])) { |
|
$mdm_category_id = $catrow['id']; |
|
} |
|
} |
|
|
|
$segs = explode($d0, $hl7); |
|
|
|
foreach ($segs as $seg) { |
|
if (empty($seg)) { |
|
continue; |
|
} |
|
if ($seg == chr(28)) { |
|
continue; |
|
} |
|
// echo "<!-- $dryrun $seg -->\n"; // debugging |
|
|
|
++$rhl7_segnum; |
|
$a = explode($d1, $seg); |
|
|
|
if ($a[0] == 'MSH') { |
|
if (!$dryrun) { |
|
rhl7FlushMain($amain, $commentdelim); |
|
} |
|
|
|
$amain = array(); |
|
|
|
if ('MDM' == $msgtype && !$dryrun) { |
|
$rc = rhl7FlushMDM( |
|
$patient_id, |
|
$mdm_docname, |
|
$mdm_datetime, |
|
$mdm_text, |
|
$mdm_category_id, |
|
$oprow ? $oprow['username'] : 0 |
|
); |
|
if ($rc) { |
|
return rhl7LogMsg($rc); |
|
} |
|
|
|
$patient_id = 0; |
|
} |
|
|
|
$context = $a[0]; |
|
// Ensoftek: Could come is as 'ORU^R01^ORU_R01'. Handle all cases when 'ORU^R01' is seen. |
|
if (strstr($a[8], "ORU^R01")) { |
|
$msgtype = 'ORU'; |
|
} elseif ($a[8] == 'MDM^T02' || $a[8] == 'MDM^T04' || $a[8] == 'MDM^T08') { |
|
$msgtype = 'MDM'; |
|
$mdm_datetime = ''; |
|
$mdm_docname = ''; |
|
$mdm_text = ''; |
|
} elseif (strstr($a[8], "ORU")) { // Labcorp does not send trigger |
|
$msgtype = 'ORU'; |
|
} else { |
|
return rhl7LogMsg(xl('MSH.8 message type is not supported') . ": '" . $a[8] . "'", true); |
|
} |
|
|
|
$in_message_id = $a[9]; |
|
$in_message_lab_name = rhl7Text($a[3]); |
|
} elseif ($a[0] == 'PID') { |
|
$context = $a[0]; |
|
|
|
if ('MDM' == $msgtype && !$dryrun) { |
|
$rc = rhl7FlushMDM( |
|
$patient_id, |
|
$mdm_docname, |
|
$mdm_datetime, |
|
$mdm_text, |
|
$mdm_category_id, |
|
$oprow ? $oprow['username'] : 0 |
|
); |
|
if ($rc) { |
|
return rhl7LogMsg($rc); |
|
} |
|
} |
|
$porow = false; |
|
$pcrow = false; |
|
$oprow = false; |
|
$in_orderid = 0; |
|
$in_ssn = preg_replace('/[^0-9]/', '', $a[4]); |
|
$in_dob = rhl7Date($a[7]); |
|
// foreign MRN |
|
$in_pubpid = rhl7Text($a[3] ?? ''); |
|
$tmp = explode($d2, $a[11]); |
|
$in_street = rhl7Text($tmp[0]) ?? ''; |
|
$in_street1 = rhl7Text($tmp[1] ?? ''); |
|
$in_city = rhl7Text($tmp[2] ?? ''); |
|
$in_state = rhl7Text($tmp[3] ?? ''); |
|
$in_zip = rhl7Text($tmp[4] ?? ''); |
|
$in_phone = rhl7Text($a[13]) ?? ''; |
|
switch (strtoupper($a[8])) { |
|
case 'M': |
|
$in_sex = 'Male'; |
|
break; |
|
case 'F': |
|
$in_sex = 'Female'; |
|
break; |
|
case 'T': |
|
$in_sex = 'Transgender'; |
|
break; |
|
default: |
|
$in_sex = 'Unassigned'; |
|
} |
|
$tmp = explode($d2, $a[5]); |
|
$in_lname = rhl7Text($tmp[0]); |
|
$in_fname = rhl7Text($tmp[1]); |
|
$in_mname = rhl7Text($tmp[2] ?? ''); |
|
|
|
$patient_id = 0; |
|
|
|
// Patient matching is needed for a results-only interface or MDM message type. |
|
if ('R' == $direction || 'MDM' == $msgtype) { |
|
$ptarr = array( |
|
'ss' => strtoupper($in_ssn), |
|
'fname' => ucname($in_fname), |
|
'lname' => ucname($in_lname), |
|
'mname' => ucname($in_mname), |
|
'DOB' => strtoupper($in_dob), |
|
'sex' => $in_sex, |
|
'street' => $in_street, |
|
'city' => $in_city, |
|
'state' => $in_state, |
|
'postal_code' => $in_zip, |
|
'phone_home' => $in_phone, |
|
'pubpid' => $in_pubpid |
|
); |
|
$patient_id = match_patient($ptarr); |
|
if ($patient_id == -1) { |
|
// Result is indeterminate. |
|
// Make a stringified form of $ptarr to use as a key. |
|
$ptstring = serialize($ptarr); |
|
// Check if the user has specified the patient. |
|
if (isset($matchresp[$ptstring])) { |
|
// This will be an existing pid, or 0 to specify creating a patient. |
|
$patient_id = intval($matchresp[$ptstring]); |
|
} else { |
|
if ($dryrun) { |
|
// Nope, ask the user to match. |
|
$matchreq[$ptstring] = true; |
|
$rhl7_return['needmatch'] = true; |
|
} else { |
|
// Should not happen, but it would be bad to abort now. Create the patient. |
|
$patient_id = 0; |
|
rhl7LogMsg(xl('Unexpected non-match, creating new patient for segment') . |
|
' ' . $rhl7_segnum, false); |
|
} |
|
} |
|
} |
|
|
|
if ($patient_id == 0 && !$dryrun) { |
|
// We must create the patient. |
|
$patient_id = create_skeleton_patient($ptarr); |
|
} |
|
|
|
if ($patient_id == -1) { |
|
$patient_id = 0; |
|
} |
|
} // end results-only/MDM logic |
|
} elseif ('PD1' == $a[0]) { |
|
// TBD: Save primary care provider name ($a[4]) somewhere? |
|
} elseif ('PV1' == $a[0]) { |
|
if ('ORU' == $msgtype) { |
|
// Save placer encounter number if present. |
|
if ($direction != 'R' && !empty($a[19])) { |
|
$tmp = explode($d2, $a[19]); |
|
$in_encounter = intval($tmp[0]); |
|
} |
|
} elseif ('MDM' == $msgtype) { |
|
// For documents we want the ordering provider. |
|
// Try Referring Provider first. |
|
$oprow = match_provider(explode($d2, $a[8])); |
|
// If no match, try Other Provider. |
|
if (empty($oprow)) { |
|
$oprow = match_provider(explode($d2, $a[52])); |
|
} |
|
} |
|
} elseif ('ORC' == $a[0] && 'ORU' == $msgtype) { |
|
$context = $a[0]; |
|
$arep = array(); |
|
$porow = false; |
|
$pcrow = false; |
|
if ($direction != 'R' && $a[2]) { |
|
$in_orderid = intval($a[2]); |
|
} |
|
} elseif ('TXA' == $a[0] && 'MDM' == $msgtype) { |
|
$context = $a[0]; |
|
$mdm_datetime = rhl7DateTime($a[4]); |
|
$mdm_docname = rhl7Text($a[12]); |
|
} elseif ($a[0] == 'NTE' && ($context == 'ORC' || $context == 'TXA')) { |
|
// Is this ever used? |
|
} elseif ('OBR' == $a[0] && 'ORU' == $msgtype) { |
|
$context = $a[0]; |
|
$arep = array(); |
|
if ($direction != 'R' && $a[2]) { |
|
$in_orderid = intval($a[2]); |
|
$porow = false; |
|
$pcrow = false; |
|
} |
|
|
|
$tmp = explode($d2, $a[4]); |
|
$in_procedure_code = $tmp[0]; |
|
$in_procedure_name = rhl7Text($tmp[1]); |
|
$in_report_status = rhl7ReportStatus($a[25]); |
|
if ($lab_npi == "QUEST") { |
|
// for profiles that may return dif component codes for profile code. |
|
$tmp = explode($d2, $a[20]); |
|
if (!empty($tmp[3]) && !empty($tmp[4])) { |
|
$in_procedure_code = $tmp[3]; |
|
$in_procedure_name = $tmp[4]; |
|
} |
|
} |
|
// Filler identifier is supposed to be unique for each incoming report. |
|
$in_filler_id = $a[3]; |
|
// Child results will have these pointers to their parent. |
|
$in_parent_obrkey = ''; |
|
$in_parent_obxkey = ''; |
|
$parent_arep = false; // parent report, if any |
|
$parent_ares = false; // parent result, if any |
|
if (!empty($a[29])) { |
|
// This is a child so there should be a parent. |
|
$tmp = explode($d2, $a[29]); |
|
$in_parent_obrkey = str_replace($d5, $d2, $tmp[1]); |
|
$tmp = explode($d2, $a[26]); |
|
$in_parent_obxkey = str_replace($d5, $d2, $tmp[0]) . $d1 . $tmp[1]; |
|
// Look for the parent report. |
|
foreach ($amain as $arr) { |
|
if (isset($arr['fid']) && $arr['fid'] == $in_parent_obrkey) { |
|
$parent_arep = $arr['rep']; |
|
// Now look for the parent result within that report. |
|
foreach ($arr['res'] as $tmpres) { |
|
if (isset($tmpres['obxkey']) && $tmpres['obxkey'] == $in_parent_obxkey) { |
|
$parent_ares = $tmpres; |
|
break; |
|
} |
|
} |
|
|
|
break; |
|
} |
|
} |
|
} |
|
|
|
if ($parent_arep) { |
|
$in_orderid = $parent_arep['procedure_order_id']; |
|
} |
|
|
|
if ($direction == 'R') { |
|
// Save their order ID to procedure_order.control_id. |
|
// Look for an existing order using that plus lab_id. |
|
// Ordering provider is OBR.16 (NPI^Last^First). |
|
// Might not need to create a dummy encounter. |
|
// Need also provider_id (probably), patient_id, date_ordered, lab_id. |
|
// We have observation date/time in OBR.7. |
|
// We have report date/time in OBR.22. |
|
// We do not have an order date. |
|
|
|
$external_order_id = empty((int)$a[2]) ? $a[3] : $a[2]; |
|
$porow = false; |
|
|
|
if (!$in_orderid && $external_order_id) { |
|
$porow = sqlQuery( |
|
"SELECT * FROM procedure_order " . |
|
"WHERE lab_id = ? AND control_id = ? " . |
|
"ORDER BY procedure_order_id DESC LIMIT 1", |
|
array($lab_id, $external_order_id) |
|
); |
|
} |
|
|
|
if (!empty($porow)) { |
|
$in_orderid = intval($porow['procedure_order_id']); |
|
} |
|
|
|
if (!$in_orderid) { |
|
// Create order. |
|
// Need to identify the ordering provider and, if possible, a recent encounter. |
|
$datetime_report = rhl7DateTime($a[22]); |
|
$date_report = substr($datetime_report, 0, 10) . ' 00:00:00'; |
|
$encounter_id = 0; |
|
$provider_id = 0; |
|
$external_id = rhl7Text($a[3]) ?? null; |
|
// Look for the most recent encounter within 30 days of the report date. |
|
$encrow = sqlQuery( |
|
"SELECT encounter FROM form_encounter WHERE " . |
|
"pid = ? AND date <= ? AND DATE_ADD(date, INTERVAL 30 DAY) > ? " . |
|
"ORDER BY date DESC, encounter DESC LIMIT 1", |
|
array($patient_id, $date_report, $date_report) |
|
); |
|
if (!empty($encrow)) { |
|
$encounter_id = intval($encrow['encounter']); |
|
$provider_id = intval($encrow['provider_id'] ?? ''); |
|
} |
|
|
|
if (!$provider_id) { |
|
// Attempt ordering provider matching by name or NPI. |
|
$oprow = match_provider(explode($d2, $a[16])); |
|
if (!empty($oprow)) { |
|
$provider_id = (int)$oprow['id']; |
|
$provider_username = $oprow['username']; |
|
} |
|
} |
|
|
|
if (!$dryrun) { |
|
// create an encounter. I mean, why not... |
|
if (empty($encrow) && !$encounter_id) { |
|
$encounter_id = create_encounter( |
|
$patient_id, |
|
$provider_id, |
|
$datetime_report, |
|
$in_message_lab_name |
|
); |
|
} |
|
// Now create the procedure order. |
|
$in_orderid = sqlInsert( |
|
"INSERT INTO procedure_order SET " . |
|
"date_ordered = ?, " . |
|
"provider_id = ?, " . |
|
"lab_id = ?, " . |
|
"date_collected = ?, " . |
|
"date_transmitted = ?, " . |
|
"patient_id = ?, " . |
|
"encounter_id = ?, " . |
|
"control_id = ?, " . |
|
"external_id = ?", |
|
array( |
|
$datetime_report, |
|
$provider_id, |
|
$lab_id, |
|
rhl7DateTime($a[22]), |
|
rhl7DateTime($a[7]), |
|
$patient_id, |
|
$encounter_id, |
|
$external_order_id, |
|
$external_id |
|
) |
|
); |
|
// If an encounter was identified then link the order to it. |
|
$form_name = ($lab_npi > '' ? $lab_npi : "Auto Result") . "-" . $in_orderid; |
|
if ($encounter_id && $in_orderid) { |
|
addForm($encounter_id, $form_name, $in_orderid, "procedure_order", $patient_id); |
|
} |
|
// create a note to provider about these actions |
|
$txdate = rhl7DateTime($a[7]); |
|
$ptext = "$orphanLog $form_name Order ordered by" . ($provider_username ?? '') . |
|
" with lab result file " . |
|
"creation date on $datetime_report and specimen collections on $txdate has been created. " . |
|
"Please review these items to ensure proper resolution of order results."; |
|
$dumb = labNotice($patient_id, $ptext, ($provider_username ?? ''), '', $in_message_lab_name); |
|
} |
|
} // end no $porow |
|
} // end results-only |
|
if (empty($porow)) { |
|
$porow = sqlQuery("SELECT * FROM procedure_order WHERE " . |
|
"procedure_order_id = ?", array($in_orderid)); |
|
// The order must already exist. Currently we do not handle electronic |
|
// results returned for manual orders. |
|
if (empty($porow) && !($dryrun && $direction == 'R')) { |
|
return rhl7LogMsg(xl('Procedure order not found') . ": $in_orderid", true); |
|
} |
|
|
|
if ($in_encounter) { |
|
if ($direction != 'R' && $porow['encounter_id'] != $in_encounter) { |
|
return rhl7LogMsg(xl('Encounter ID') . |
|
" '" . $porow['encounter_id'] . "' " . |
|
xl('for OBR placer order number') . |
|
" '$in_orderid' " . |
|
xl('does not match the PV1 encounter number') . |
|
" '$in_encounter'"); |
|
} |
|
} else { |
|
// They did not return an encounter number to verify, so more checking |
|
// might be done here to make sure the patient seems to match. |
|
} |
|
|
|
// Save the lab's control ID if there is one. |
|
$tmp = explode($d2, $a[3]); |
|
$control_id = $tmp[0]; |
|
if ($control_id && empty($porow['control_id']) && $in_orderid) { |
|
sqlStatement("UPDATE procedure_order SET control_id = ? WHERE " . |
|
"procedure_order_id = ?", array($control_id, $in_orderid)); |
|
} |
|
|
|
$code_seq_array = array(); |
|
} |
|
|
|
// Find the order line item (procedure code) that matches this result. |
|
// If there is more than one, then we select the one whose sequence number |
|
// is next after the last sequence number encountered for this procedure |
|
// code; this assumes that result OBRs are returned in the same sequence |
|
// as the corresponding OBRs in the order. |
|
if (!isset($code_seq_array[$in_procedure_code])) { |
|
$code_seq_array[$in_procedure_code] = 0; |
|
} |
|
|
|
$pcquery = "SELECT pc.* FROM procedure_order_code AS pc " . |
|
"WHERE pc.procedure_order_id = ? AND pc.procedure_code = ? " . |
|
"ORDER BY (procedure_order_seq <= ?), procedure_order_seq LIMIT 1"; |
|
$pcqueryargs = array($in_orderid, $in_procedure_code, $code_seq_array[$in_procedure_code]); |
|
$pcrow = sqlQuery($pcquery, $pcqueryargs); |
|
if (empty($pcrow)) { |
|
// There is no matching procedure in the order, so it must have been |
|
// added after the original order was sent, either as a manual request |
|
// from the physician or as a "reflex" from the lab. |
|
// procedure_source = '2' indicates this. |
|
if (!$dryrun) { |
|
$lkup = lookupTestCode($lab_id, $in_procedure_code); |
|
$code_type = ($lkup['procedure_type'] ?? '') ? trim($lkup['procedure_type']) : ''; |
|
$code_transport = ($lkup['transport'] ?? '') ? trim($lkup['transport']) : ''; |
|
sqlBeginTrans(); |
|
$procedure_order_seq = sqlQuery( |
|
"SELECT IFNULL(MAX(procedure_order_seq),0) + 1 AS increment FROM procedure_order_code " . |
|
"WHERE procedure_order_id = ? ", |
|
array($in_orderid) |
|
); |
|
sqlInsert( |
|
"INSERT INTO procedure_order_code SET " . |
|
"procedure_order_id = ?, " . |
|
"procedure_order_seq = ?, " . |
|
"procedure_code = ?, " . |
|
"procedure_name = ?, " . |
|
"procedure_type = ?, " . |
|
"transport = ?, " . |
|
"procedure_source = '2'", |
|
array( |
|
$in_orderid, |
|
$procedure_order_seq['increment'], |
|
$in_procedure_code, |
|
$in_procedure_name, |
|
$code_type, |
|
$code_transport |
|
) |
|
); |
|
$pcrow = sqlQuery($pcquery, $pcqueryargs); |
|
sqlCommitTrans(); |
|
} else { |
|
// Dry run, make a dummy procedure_order_code row. |
|
$pcrow = array( |
|
'procedure_order_id' => $in_orderid, |
|
'procedure_order_seq' => 0, // TBD? |
|
); |
|
} |
|
} |
|
if (!empty($a[21])) { |
|
$obrPerformingOrganization = getPerformingOrganizationDetails('', $a[21], '', $d2, $commentdelim); |
|
} |
|
$code_seq_array[$in_procedure_code] = 0 + $pcrow['procedure_order_seq']; |
|
$arep = array(); |
|
$arep['procedure_order_id'] = $in_orderid; |
|
$arep['procedure_order_seq'] = $pcrow['procedure_order_seq']; |
|
$arep['date_collected'] = rhl7DateTime($a[7]); |
|
$arep['date_collected_tz'] = rhl7DateTimeZone($a[7]); |
|
$arep['date_report'] = rhl7DateTime($a[22]); |
|
$arep['date_report_tz'] = rhl7DateTimeZone($a[22]); |
|
$arep['report_status'] = $in_report_status; |
|
$arep['report_notes'] = ''; |
|
$arep['specimen_num'] = ''; |
|
|
|
// If this is a child report, add some info from the parent. |
|
if (!empty($parent_ares)) { |
|
$arep['report_notes'] .= xl('This is a child of result') . ' ' . |
|
$parent_ares['result_code'] . ' ' . xl('with value') . ' "' . |
|
$parent_ares['result'] . '".' . "\n"; |
|
} |
|
|
|
if (!empty($parent_arep)) { |
|
$arep['report_notes'] .= $parent_arep['report_notes']; |
|
$arep['specimen_num'] = $parent_arep['specimen_num']; |
|
} |
|
|
|
// Create the main array entry for this report and its results. |
|
$i = count($amain); |
|
$amain[$i] = array(); |
|
$amain[$i]['rep'] = $arep; |
|
$amain[$i]['fid'] = $in_filler_id; |
|
$amain[$i]['res'] = array(); |
|
} elseif ($a[0] == 'NTE' && $context == 'OBR') { |
|
// Append this note to those for the most recent report. |
|
$amain[count($amain) - 1]['rep']['report_notes'] .= rhl7Text($a[3], true) . "\n"; |
|
} elseif ('OBX' == $a[0] && 'ORU' == $msgtype) { |
|
$tmp = explode($d2, $a[3]); |
|
$result_code = rhl7Text($tmp[0]); |
|
$result_text = rhl7Text($tmp[1]); |
|
if ($lab_npi == "QUEST") { |
|
if (empty($result_code)) { |
|
$result_code = rhl7Text($tmp[3]); |
|
} |
|
$result_text = rhl7Text($tmp[4]); |
|
} |
|
// If this is a text result that duplicates the previous result except |
|
// for its value, then treat it as an extension of that result's value. |
|
$i = count($amain) - 1; |
|
$j = count($amain[$i]['res']) - 1; |
|
if ( |
|
$j >= 0 && $context == 'OBX' && $a[2] == 'TX' |
|
&& $amain[$i]['res'][$j]['result_data_type'] == 'L' |
|
&& $amain[$i]['res'][$j]['result_code'] == $result_code |
|
&& $amain[$i]['res'][$j]['date'] == rhl7DateTime($a[14] ?? '') |
|
&& ($amain[$i]['res'][$j]['facility'] ?? '') == rhl7Text($a[15] ?? '') |
|
&& $amain[$i]['res'][$j]['abnormal'] == rhl7Abnormal($a[8] ?? '') |
|
&& $amain[$i]['res'][$j]['result_status'] == rhl7ReportStatus($a[11] ?? '') |
|
) { |
|
$amain[$i]['res'][$j]['comments'] = |
|
substr( |
|
$amain[$i]['res'][$j]['comments'], |
|
0, |
|
strlen($amain[$i]['res'][$j]['comments']) |
|
) . rhl7Text($a[5] ?? '') . $commentdelim; |
|
continue; |
|
} |
|
|
|
$context = $a[0]; |
|
$ares = array(); |
|
$ares['result_data_type'] = substr($a[2], 0, 1); // N, S, F or E |
|
$ares['comments'] = $commentdelim; |
|
if ($a[2] == 'ED') { |
|
// This is the case of results as an embedded document. We will create |
|
// a normal patient document in the assigned category for lab results. |
|
$tmp = explode($d2, $a[5]); |
|
$fileext = strtolower($tmp[2]); |
|
$filename = date("Ymd_His") . '_orderid_' . $in_orderid . '.' . $fileext; |
|
if ($tmp[4] != 'EMBEDDED DOCUMENT') { // an OBX indicating a document in next Seg ZEF so pass this by. |
|
$data = rhl7DecodeData($tmp[3], $tmp[4]); |
|
if ($data === false) { |
|
return rhl7LogMsg(xl('Invalid encapsulated data encoding type') . ': ' . $tmp[3]); |
|
} |
|
if (!$dryrun) { |
|
$d = new Document(); |
|
$rc = $d->createDocument( |
|
$porow['patient_id'], |
|
$results_category_id, // TBD: Make sure not 0 |
|
$filename, |
|
rhl7MimeType($fileext), |
|
$data |
|
); |
|
if ($rc) { |
|
return rhl7LogMsg($rc); |
|
} |
|
|
|
$ares['document_id'] = $d->get_id(); |
|
} |
|
} // @todo suspect below!! |
|
$ares['date'] = $arep['date_report']; // $arep is left over from the OBR logic. |
|
// Append this result to those for the most recent report. |
|
// Note the 'procedure_report_id' item is not yet present. |
|
//$amain[count($amain) - 1]['res'][] = $ares; |
|
} elseif ($a[2] == 'CWE') { |
|
$ares['result'] = rhl7CWE($a[5], $d2); |
|
} elseif ($a[2] == 'SN') { |
|
$ares['result'] = trim(str_replace($d2, ' ', $a[5])); |
|
} elseif ($a[2] == 'TX' || strlen($a[5]) > 200) { |
|
// OBX-5 can be a very long string of text with "~" as line separators. |
|
// The first line of comments is reserved for such things. |
|
$ares['result_data_type'] = 'L'; |
|
$ares['result'] = ''; |
|
if (empty($a[5])) { |
|
$vTx = rhl7Text(str_replace('^', ' ', $a[3])); |
|
} else { |
|
$vTx = rhl7Text($a[5]); |
|
} |
|
$ares['comments'] = $vTx . $commentdelim; |
|
} else { |
|
$ares['result'] = rhl7Text($a[5]); |
|
} |
|
|
|
$ares['result_code'] = $result_code; |
|
$ares['result_text'] = $result_text; |
|
$ares['date'] = rhl7DateTime($a[14] ?? ''); |
|
//$ares['facility'] = rhl7Text($a[15]); |
|
// Ensoftek: Units may have mutiple segments(as seen in MU2 samples), parse and take just first segment. |
|
$tmp = explode($d2, ($a[6] ?? '')); |
|
$ares['units'] = rhl7Text($tmp[0]); |
|
$ares['range'] = rhl7Text($a[7] ?? ''); |
|
$ares['abnormal'] = rhl7Abnormal($a[8] ?? ''); // values are lab dependent |
|
$ares['result_status'] = rhl7ReportStatus($a[11] ?? ''); |
|
|
|
// Ensoftek: Performing Organization Details. |
|
// Goes into "Pending Review/Patient Results--->Notes--->Facility" section. |
|
if (empty($obrPerformingOrganization)) { |
|
$performingOrganization = getPerformingOrganizationDetails( |
|
$a[23] ?? '', |
|
$a[24] ?? '', |
|
$a[25] ?? '', |
|
$d2, |
|
$commentdelim |
|
); |
|
} else { |
|
$performingOrganization = $obrPerformingOrganization; |
|
} |
|
if (!empty($performingOrganization)) { |
|
$ares['facility'] .= $performingOrganization . $commentdelim; |
|
} |
|
|
|
/**** |
|
* // Probably need a better way to report this, if it matters. |
|
* if (!empty($a[19])) { |
|
* $ares['comments'] .= xl('Analyzed') . ' ' . rhl7DateTime($a[19]) . '.' . $commentdelim; |
|
* } |
|
****/ |
|
|
|
// obxkey is to allow matching this as a parent result. |
|
$ares['obxkey'] = $a[3] . $d1 . ($a[4] ?? ''); |
|
|
|
// Append this result to those for the most recent report. |
|
// Note the 'procedure_report_id' item is not yet present. |
|
$amain[count($amain) - 1]['res'][] = $ares; |
|
} elseif ('OBX' == $a[0] && 'MDM' == $msgtype) { |
|
$context = $a[0]; |
|
if ($a[2] == 'TX') { |
|
if ($mdm_text !== '') { |
|
$mdm_text .= "\r\n"; |
|
} |
|
|
|
$mdm_text .= rhl7Text($a[5]); |
|
} else { |
|
return rhl7LogMsg(xl('Unsupported MDM OBX result type') . ': ' . $a[2]); |
|
} |
|
} elseif ('ZEF' == $a[0] && 'ORU' == $msgtype) { |
|
// ZEF segment is treated like an OBX with an embedded Base64-encoded PDF. |
|
$context = 'OBX'; |
|
$ares = array(); |
|
$ares['result_data_type'] = 'E'; |
|
$ares['comments'] = $commentdelim; |
|
// |
|
$fileext = 'pdf'; |
|
$filename = date("Ymd_His") . '_orderid_' . $in_orderid . '.' . $fileext; |
|
$data = rhl7DecodeData('Base64', $a[2]); |
|
if ($data === false) { |
|
return rhl7LogMsg(xl('ZEF segment internal error')); |
|
} |
|
|
|
if (!$dryrun) { |
|
$d = new Document(); |
|
$rc = $d->createDocument( |
|
$porow['patient_id'], |
|
$results_category_id, // TBD: Make sure not 0 |
|
$filename, |
|
rhl7MimeType($fileext), |
|
$data |
|
); |
|
if ($rc) { |
|
return rhl7LogMsg($rc); |
|
} |
|
|
|
$ares['document_id'] = $d->get_id(); |
|
} |
|
|
|
$ares['date'] = $arep['date_report']; // $arep is left over from the OBR logic. |
|
// Append this result to those for the most recent report. |
|
// Note the 'procedure_report_id' item is not yet present. |
|
$amain[count($amain) - 1]['res'][] = $ares; |
|
} elseif ('NTE' == $a[0] && 'OBX' == $context && 'ORU' == $msgtype) { |
|
// Append this note to the most recent result item's comments. |
|
$alast = count($amain) - 1; |
|
$rlast = count($amain[$alast]['res']) - 1; |
|
$amain[$alast]['res'][$rlast]['comments'] .= rhl7Text($a[3] ?? '', true) . $commentdelim; |
|
// Ensoftek: Get data from SPM segment for specimen. |
|
// SPM segment always occurs after the OBX segment. |
|
} elseif ('SPM' == $a[0] && 'ORU' == $msgtype) { |
|
rhl7UpdateReportWithSpecimen($amain, $a, $d2); |
|
// Add code here for any other segment types that may be present. |
|
// Ensoftek: Get data from SPM segment for specimen. Comes in with MU2 samples, but can be ignored. |
|
} elseif ('TQ1' == $a[0] && 'ORU' == $msgtype) { |
|
// Ignore and do nothing. |
|
} elseif ($a[0] == 'NTE' && 'PID' == $context) { |
|
// will get orderid on save. |
|
if (!empty($amain[0])) { |
|
$amain[0]['rep']['report_notes'] .= rhl7Text($a[3], true) . "\n"; |
|
} |
|
} elseif ('ZPS' == $a[0] && 'ORU' == $msgtype) { |
|
//global $ares; |
|
$performingOrganization = parseZPS($a); |
|
if (!empty($performingOrganization)) { |
|
$alast = count($amain) - 1; |
|
$amain[$alast]['res'][0]['facility'] .= $performingOrganization . $commentdelim; |
|
} |
|
} else { |
|
return rhl7LogMsg(xl('Segment name') . " '${a[0]}' " . xl('is misplaced or unknown')); |
|
} |
|
} |
|
|
|
// Write all reports and their results to the database. |
|
// This will do nothing if a dry run or MDM message type. |
|
if ('ORU' == $msgtype && !$dryrun) { |
|
rhl7FlushMain($amain, $commentdelim); |
|
} |
|
|
|
if ('MDM' == $msgtype && !$dryrun) { |
|
// Write documents. |
|
$rc = rhl7FlushMDM( |
|
$patient_id, |
|
$mdm_docname, |
|
$mdm_datetime, |
|
$mdm_text, |
|
$mdm_category_id, |
|
$oprow ? $oprow['username'] : 0 |
|
); |
|
if ($rc) { |
|
return rhl7LogMsg($rc); |
|
} |
|
} |
|
|
|
return $rhl7_return; |
|
} |
|
|
|
/** |
|
* Poll all eligible labs for new results and store them in the database. |
|
* |
|
* @param array &$info Conveys information to and from the caller: |
|
* FROM THE CALLER: |
|
* $info["$ppid/$filename"]['delete'] = a non-empty value if file deletion is requested. |
|
* $info['select'] = array of patient matching responses where key is serialized patient |
|
* attributes and value is selected pid for this patient, or 0 to create the patient. |
|
* TO THE CALLER: |
|
* $info["$ppid/$filename"]['mssgs'] = array of messages from this function. |
|
* $info['match'] = array of patient matching requests where key is serialized patient |
|
* attributes (ss, fname, lname, DOB) and value is TRUE (irrelevant). |
|
* |
|
* @return string Error text, or empty if no errors. |
|
*/ |
|
function poll_hl7_results(&$info, $labs = 0) |
|
{ |
|
global $srcdir, $orphanLog, $lab_npi; |
|
$labs = (int)$labs + 0; |
|
// echo "<!-- post: "; print_r($_POST); echo " -->\n"; // debugging |
|
// echo "<!-- in: "; print_r($info); echo " -->\n"; // debugging |
|
|
|
$filecount = 0; |
|
$badcount = 0; |
|
$maxdl = $_REQUEST['form_max_results'] ?? 9999; // in case to prevent not running report. |
|
if (!isset($info['match'])) { |
|
$info['match'] = array(); // match requests |
|
} |
|
|
|
if (!isset($info['select'])) { |
|
$info['select'] = array(); // match request responses |
|
} |
|
|
|
$ppres = sqlStatement("SELECT * FROM procedure_providers ORDER BY name"); |
|
|
|
while ($pprow = sqlFetchArray($ppres)) { |
|
$ppid = (int)$pprow['ppid']; |
|
$protocol = $pprow['protocol']; |
|
$remote_host = $pprow['remote_host']; |
|
$send_account = $pprow['send_fac_id']; |
|
$recv_account = $pprow['recv_fac_id']; |
|
$lab_app = $pprow['recv_app_id']; |
|
$lab_name = $pprow['name']; |
|
$lab_npi = strtoupper(trim($pprow['npi'])); |
|
$debug = trim($pprow['DorP']) === 'D'; |
|
$hl7 = ''; |
|
$orphanLog = ''; |
|
$log = ''; |
|
$logpath = $GLOBALS['OE_SITE_DIR'] . "/documents/procedure_results/logs/$lab_npi"; |
|
|
|
if ($ppid !== $labs && $labs > 0) { |
|
continue; |
|
} |
|
$filecount = 0; |
|
// Support manual results when doing electronic orders |
|
// without an existing patient and/or procedure and/or order. |
|
// |
|
if (isset($info['orphaned_order'])) { |
|
$pprow['direction'] = $info['orphaned_order']; // manual results or order not found |
|
} |
|
|
|
if ($protocol == 'SFTP') { |
|
$remote_port = 22; |
|
// Hostname may have ":port" appended to specify a nonstandard port number. |
|
if ($i = strrpos($remote_host, ':')) { |
|
$remote_port = (int)substr($remote_host, $i + 1); |
|
$remote_host = substr($remote_host, 0, $i); |
|
} |
|
|
|
// Compute the target path name. |
|
$pathname = '.'; |
|
if ($pprow['results_path']) { |
|
$pathname = $pprow['results_path'] . '/' . $pathname; |
|
} |
|
|
|
// Connect to the server and enumerate files to process. |
|
$sftp = new SFTP($remote_host, $remote_port); |
|
if (!$sftp->login($pprow['login'], $pprow['password'])) { |
|
return xl('Login to remote host') . " '$remote_host' " . xl('failed'); |
|
} |
|
|
|
$files = $sftp->nlist($pathname); |
|
foreach ($files as $file) { |
|
if (str_starts_with($file, '.')) { |
|
continue; |
|
} |
|
|
|
if (!isset($info["$lab_name/$ppid/$file"])) { |
|
$info["$lab_name/$ppid/$file"] = array(); |
|
} |
|
|
|
// Ensure that archive directory exists. |
|
$prpath = $GLOBALS['OE_SITE_DIR'] . "/documents/procedure_results"; |
|
if (!file_exists($prpath)) { |
|
if (!mkdir($prpath, 0755, true) && !is_dir($prpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $prpath)); |
|
} |
|
} |
|
|
|
$prpath .= '/' . $pprow['ppid'] . '-' . $pprow['npi']; |
|
if (!file_exists($prpath)) { |
|
if (!mkdir($prpath, 0755, true) && !is_dir($prpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $prpath)); |
|
} |
|
} |
|
|
|
// Get file contents. |
|
$hl7 = $sftp->get("$pathname/$file"); |
|
|
|
++$filecount; |
|
$fh = fopen("$prpath/$file", 'w'); |
|
if ($fh) { |
|
// Store the file, encrypted when enabled |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
$log .= "Retrieved and Saved File #$filecount: $file\n"; |
|
} else { |
|
$log .= "Retrieved but Couldn't Save File #$filecount: $file\n"; |
|
//return xl('Cannot create file') . ' "' . "$prpath/$file" . '"'; |
|
} |
|
|
|
// If user requested reject and delete, do that. |
|
if (!empty($info["$lab_name/$ppid/$file"]['delete'])) { |
|
$fh = fopen("$prpath/$file.rejected", 'w'); |
|
if ($fh) { |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
} else { |
|
$log .= "Retrieved but Couldn't Save Rejected File #$filecount: $file\n"; |
|
//return xl('Cannot create file') . ' "' . "$prpath/$file.rejected" . '"'; |
|
} |
|
|
|
if (strtoupper($lab_npi) != 'LABCORP') { // this is nuts |
|
if (!$sftp->delete("$pathname/$file")) { |
|
return xl('Cannot delete (from SFTP server) file') . ' "' . "$pathname/$file" . '"'; |
|
} |
|
} |
|
continue; |
|
} |
|
|
|
if (!match_lab($hl7, $send_account, $recv_account, $lab_app, $lab_npi)) { |
|
$log .= "No Lab Account Matched for File #$filecount: Account: $send_account\n"; |
|
continue; |
|
} |
|
if ($filecount > $maxdl) { |
|
break; |
|
} |
|
// Do a dry run of its contents and check for errors and match requests. |
|
$tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], true, $info['select']); |
|
$log .= "Lab matched account $send_account. Results Dry Run Parse for Errors: " . |
|
$tmp['mssgs'] ? print_r($tmp['mssgs'], true) : "None" . "\n"; |
|
|
|
$info["$lab_name/$ppid/$file"]['mssgs'] = $tmp['mssgs']; |
|
// $info["$lab_name/$ppid/$file"]['match'] = $tmp['match']; |
|
if (!empty($tmp['fatal']) || !empty($tmp['needmatch'])) { |
|
// There are errors or matching requests so skip this file. |
|
continue; |
|
} |
|
$orphanLog = ''; |
|
// Now the money shot - not a dry run. |
|
$tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], false, $info['select']); |
|
$info["$lab_name/$ppid/$file"]['mssgs'] = $tmp['mssgs']; |
|
// $info["$lab_name/$ppid/$file"]['match'] = $tmp['match']; |
|
if (empty($tmp['fatal']) && empty($tmp['needmatch'])) { |
|
// It worked, archive and delete the file. |
|
$fh = fopen("$prpath/$file", 'w'); |
|
if ($fh) { |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
$log .= "Success Saved Results #$filecount to: $file\n"; |
|
} else { |
|
$log .= "Success but Couldn't Save File #$filecount: $file\n"; |
|
return xl('Cannot create file') . ' "' . "$prpath/$file" . '"'; |
|
} |
|
if (strtoupper($lab_npi) != 'LABCORP') { // this is nuts |
|
if (!$sftp->delete("$pathname/$file")) { |
|
return xl('Cannot delete (from SFTP server) file') . ' "' . "$pathname/$file" . '"'; |
|
} |
|
} |
|
} |
|
} // end of this file |
|
if (!file_exists($logpath)) { |
|
if (!mkdir($logpath, 0755, true) && !is_dir($logpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $logpath)); |
|
} |
|
} |
|
$logfile = "batchrun_" . date("m-d-y_His") . ".log"; |
|
$fh = fopen("$logpath/$logfile", 'w'); |
|
if ($fh) { |
|
$logadd = print_r($info, true); |
|
$log .= "Done Batch Run. Debug shows:\n" . $logadd . "\n"; |
|
fwrite($fh, $log); |
|
fclose($fh); |
|
} |
|
} elseif ($protocol == 'FS') { |
|
// Filesystem directory containing results files. |
|
$pathname = $pprow['results_path']; |
|
if (!($dh = opendir($pathname))) { |
|
return xl('Unable to access directory for lab npi:') . " $lab_npi"; |
|
} |
|
|
|
// Sort by filename just because. |
|
$files = array(); |
|
while (false !== ($file = readdir($dh))) { |
|
if (str_starts_with($file, '.')) { |
|
continue; |
|
} |
|
if (is_dir($file)) { |
|
continue; |
|
} |
|
$files[$file] = $file; |
|
} |
|
closedir($dh); |
|
ksort($files); |
|
// For each file... |
|
foreach ($files as $file) { |
|
if (!isset($info["$lab_name/$ppid/$file"])) { |
|
$info["$lab_name/$ppid/$file"] = array(); |
|
} |
|
|
|
// Ensure that archive directory exists. |
|
$prpath = $GLOBALS['OE_SITE_DIR'] . "/documents/procedure_results"; |
|
if (!file_exists($prpath) && !mkdir($prpath, 0755, true) && !is_dir($prpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $prpath)); |
|
} |
|
|
|
$prpath .= '/' . $pprow['ppid'] . '-' . $pprow['npi']; |
|
if (!file_exists($prpath) && !mkdir($prpath, 0755, true) && !is_dir($prpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $prpath)); |
|
} |
|
|
|
// Get file contents. |
|
$hl7 = file_get_contents("$pathname/$file"); |
|
|
|
++$filecount; |
|
if (empty($hl7)) { |
|
$log .= "Result file empty for some reason. #$filecount: $file\n"; |
|
continue; |
|
} |
|
|
|
$fh = fopen("$prpath/$file", 'w'); |
|
if ($fh) { |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
$log .= "Retrieved and Saved File #$filecount: $file\n"; |
|
} |
|
// If user requested reject and delete, do that. |
|
if (!empty($info["$lab_name/$ppid/$file"]['delete'])) { |
|
$fh = fopen("$prpath/$file.rejected", 'w'); |
|
if ($fh) { |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
} else { |
|
return xl('Cannot create file') . ' "' . "$prpath/$file.rejected" . '"'; |
|
} |
|
|
|
if (!unlink("$pathname/$file")) { |
|
return xl('Cannot delete file') . ' "' . "$pathname/$file" . '"'; |
|
} |
|
|
|
continue; |
|
} |
|
|
|
if (!match_lab($hl7, $send_account, $recv_account, $lab_app, $lab_npi)) { |
|
$log .= "No Lab Match File #$filecount: Account: $send_account\n"; |
|
continue; |
|
} |
|
if ($filecount > $maxdl) { |
|
break; |
|
} |
|
|
|
// Do a dry run of its contents and check for errors and match requests. |
|
$tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], true, $info['select']); |
|
if (!empty($tmp['mssgs'])) { |
|
$log .= "Lab matched account $send_account. Results Dry Run Parse for Errors: " . |
|
$tmp['mssgs'] ? print_r($tmp['mssgs'][0], true) : "None" . "\n"; |
|
} |
|
|
|
$info["$lab_name/$ppid/$file"]['mssgs'] = $tmp['mssgs']; |
|
// $info["$lab_name/$ppid/$file"]['match'] = $tmp['match']; |
|
if (!empty($tmp['fatal']) || !empty($tmp['needmatch'])) { |
|
// There are errors or matching requests so skip this file. |
|
continue; |
|
} |
|
$orphanLog = ''; |
|
// Now the money shot - not a dry run. |
|
$tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], false, $info['select']); |
|
$info["$lab_name/$ppid/$file"]['mssgs'] = $tmp['mssgs']; |
|
// $info["$lab_name/$ppid/$file"]['match'] = $tmp['match']; |
|
if (empty($tmp['fatal']) && empty($tmp['needmatch'])) { |
|
// It worked, archive and delete the file. |
|
$fh = fopen("$prpath/$file", 'w'); |
|
if ($fh) { |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
$log .= "Success Saved Results #$filecount to: $prpath/$file\n"; |
|
} else { |
|
return xl('Cannot create file') . ' "' . "$prpath/$file" . '"'; |
|
} |
|
|
|
if (!unlink("$pathname/$file")) { |
|
return xl('Cannot delete file') . ' "' . "$pathname/$file" . '"'; |
|
} |
|
} |
|
} // end of this file |
|
if (!file_exists($logpath)) { |
|
if (!mkdir($logpath, 0755, true) && !is_dir($logpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $logpath)); |
|
} |
|
} |
|
$logfile = "batchrun_" . date("m-d-y_His") . ".log"; |
|
$fh = fopen("$logpath/$logfile", 'w'); |
|
if ($fh) { |
|
$logadd = print_r($info, true); |
|
$log .= "End of Run Debug Log is:\n" . $logadd . "\n"; |
|
fwrite($fh, $log); |
|
fclose($fh); |
|
} |
|
} elseif ($protocol == 'WS' && strtoupper($lab_npi) == 'QUEST') { |
|
if ($debug) { |
|
continue; |
|
} |
|
// Compute the target path name. |
|
$pathname = '.'; |
|
if ($pprow['results_path']) { |
|
$pathname = $pprow['results_path'] . '/' . $pathname; |
|
} |
|
|
|
$client = new QuestResultClient($ppid); |
|
|
|
// Connect to the web service and enumerate files to process. |
|
|
|
$client->buildRequest(20); |
|
$order_results = $client->getResultsBatch(false); |
|
$request_id = $order_results->requestId; |
|
$results = $order_results->results; |
|
if (empty($results)) { |
|
return ''; // nothing to do |
|
} |
|
|
|
foreach ($results as $result) { |
|
$control_id = $result->HL7Message->controlId; |
|
$file = "result_" . $control_id . ".hl7"; |
|
|
|
++$filecount; |
|
if (!isset($info["$lab_name/$ppid/$file"])) { |
|
$info["$lab_name/$ppid/$file"] = array(); |
|
} |
|
|
|
// Ensure that archive directory exists. |
|
$prpath = $GLOBALS['OE_SITE_DIR'] . "/documents/procedure_results"; |
|
if (!file_exists($prpath)) { |
|
if (!mkdir($prpath, 0755, true) && !is_dir($prpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $prpath)); |
|
} |
|
} |
|
|
|
$prpath .= '/' . $pprow['ppid'] . '-' . $pprow['npi']; |
|
if (!file_exists($prpath)) { |
|
if (!mkdir($prpath, 0755, true) && !is_dir($prpath)) { |
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $prpath)); |
|
} |
|
} |
|
|
|
// Get result contents. |
|
$hl7 = $result->HL7Message->message; |
|
if (!$hl7) { |
|
break; |
|
} |
|
$segs = explode("\r", $result->HL7Message->message); |
|
$a = explode('|', $segs[0]); |
|
$client->SENDING_FACILITY = $a[5]; |
|
$client->RECEIVING_APPLICATION = $a[2]; |
|
$client->RECEIVING_FACILITY = $a[3]; |
|
unset($segs); |
|
// If user requested reject and delete, do that. |
|
if (!empty($info["$lab_name/$ppid/$file"]['delete'])) { |
|
$fh = fopen("$prpath/$file.rejected", 'w'); |
|
if ($fh) { |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
} else { |
|
return xl('Cannot create file') . ' "' . "$prpath/$file.rejected" . '"'; |
|
} |
|
|
|
$acks = $client->buildResultAck($control_id); |
|
$response = $client->sendResultAck($request_id, $acks, false); |
|
continue; |
|
} |
|
|
|
if (!match_lab($hl7, $send_account, $recv_account, $lab_app, $lab_npi)) { |
|
$log .= "No Lab Match Account: $send_account\n"; |
|
continue; |
|
} |
|
|
|
// Do a dry run of its contents and check for errors and match requests. |
|
$tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], true, $info['select']); |
|
$info["$lab_name/$ppid/$file"]['mssgs'] = $tmp['mssgs']; |
|
// $info["$lab_name/$ppid/$file"]['match'] = $tmp['match']; |
|
if (!empty($tmp['fatal']) || !empty($tmp['needmatch'])) { |
|
// There are errors or matching requests so skip this file. |
|
continue; |
|
} |
|
$orphanLog = ''; |
|
// Now the money shot - not a dry run. |
|
$tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], false, $info['select']); |
|
$info["$lab_name/$ppid/$file"]['mssgs'] = $tmp['mssgs']; |
|
if (empty($tmp['fatal']) && empty($tmp['needmatch'])) { |
|
// It worked, archive and delete the file. |
|
$fh = fopen("$prpath/$file", 'w'); |
|
if ($fh) { |
|
$hl7_crypt = hl7Crypt($hl7); |
|
fwrite($fh, $hl7_crypt); |
|
fclose($fh); |
|
} else { |
|
return xl('Cannot create file') . ' "' . "$prpath/$file" . '"'; |
|
} |
|
// remove ie ack |
|
$acks = $client->buildResultAck($control_id); |
|
$response = $client->sendResultAck($request_id, $acks, false); |
|
} |
|
} // end of this file |
|
// remove from service. |
|
} // end quest |
|
// TBD: Insert "else if ($protocol == '???') {...}" to support other protocols. |
|
} // end procedure provider |
|
|
|
// echo "<!-- out: "; print_r($info); echo " -->\n"; // debugging |
|
|
|
return ''; |
|
} |
|
|
|
/** |
|
* Encrypt the content of the hl7 file if the global is turned on. |
|
* |
|
* @param string $content The unencrypted content of the hl7. |
|
* @return string The encrypted content of the hl7 if the global is set. |
|
*/ |
|
function hl7Crypt($content) |
|
{ |
|
if ($GLOBALS['drive_encryption']) { |
|
$content = (new CryptoGen())->encryptStandard($content, null, 'database'); |
|
} |
|
|
|
return $content; |
|
}
|
|
|