/** * Parse and save. * * @param string &$pprow A row from the procedure_providers table. * @param string &$hl7 The input HL7 text. * @return string Error text, or empty if no errors. */ function receive_hl7_results(&$hl7) { if (substr($hl7, 0, 3) != 'MSH') { return xl('Input does not begin with a MSH segment'); } // End-of-line delimiter for text in procedure_result.comments $commentdelim = "\n"; $today = time(); $in_message_id = ''; $in_ssn = ''; $in_dob = ''; $in_lname = ''; $in_fname = ''; $in_orderid = 0; $in_procedure_code = ''; $in_report_status = ''; $in_encounter = 0; $porow = false; $pcrow = false; $procedure_report_id = 0; $arep = array(); // holding area for OBR and its NTE data $ares = array(); // holding area for OBX and its NTE data // This is so we know where we are if a segment like NTE that can appear in // different places is encountered. $context = ''; // Delimiters $d0 = "\r"; $d1 = substr($hl7, 3, 1); // typically | $d2 = substr($hl7, 4, 1); // typically ^ $d3 = substr($hl7, 5, 1); // typically ~ $segs = explode($d0, $hl7); foreach ($segs as $seg) { if (empty($seg)) { continue; } $a = explode($d1, $seg); if ($a[0] == 'MSH') { $context = $a[0]; if ($a[8] != 'ORU^R01') { return xl('Message type') . " '{$a[8]}' " . xl('does not seem valid'); } $in_message_id = $a[9]; } else { if ($a[0] == 'PID') { $context = $a[0]; rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); $in_ssn = $a[4]; $in_dob = $a[7]; // yyyymmdd format $tmp = explode($d2, $a[5]); $in_lname = $tmp[0]; $in_fname = $tmp[1]; } else { if ($a[0] == 'PV1') { // Save placer encounter number if present. if (!empty($a[19])) { $tmp = explode($d2, $a[19]); $in_encounter = intval($tmp[0]); } } else { if ($a[0] == 'ORC') { $context = $a[0]; rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); $porow = false; $pcrow = false; if ($a[2]) { $in_orderid = intval($a[2]); } } else { if ($a[0] == 'NTE' && $context == 'ORC') { // TBD? Is this ever used? } else { if ($a[0] == 'OBR') { $context = $a[0]; rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); $procedure_report_id = 0; if ($a[2]) { $in_orderid = intval($a[2]); } $tmp = explode($d2, $a[4]); $in_procedure_code = $tmp[0]; $in_procedure_name = $tmp[1]; $in_report_status = rhl7ReportStatus($a[25]); 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)) { return xl('Procedure order') . " '{$in_orderid}' " . xl('was not found'); } if ($in_encounter) { if ($porow['encounter_id'] != $in_encounter) { return 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. } } // Find the order line item (procedure code) that matches this result. $pcquery = "SELECT pc.* FROM procedure_order_code AS pc " . "WHERE pc.procedure_order_id = ? AND pc.procedure_code = ? " . "ORDER BY procedure_order_seq LIMIT 1"; $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code)); 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. sqlInsert("INSERT INTO procedure_order_code SET " . "procedure_order_id = ?, " . "procedure_code = ?, " . "procedure_name = ?, " . "procedure_source = '2'", array($in_orderid, $in_procedure_code, $in_procedure_name)); $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code)); } $arep = array(); $arep['procedure_order_id'] = $in_orderid; $arep['procedure_order_seq'] = $pcrow['procedure_order_seq']; $arep['date_collected'] = rhl7DateTime($a[7]); $arep['date_report'] = substr(rhl7DateTime($a[22]), 0, 10); $arep['report_status'] = $in_report_status; $arep['report_notes'] = ''; } else { if ($a[0] == 'NTE' && $context == 'OBR') { $arep['report_notes'] .= rhl7Text($a[3]) . "\n"; } else { if ($a[0] == 'OBX') { $context = $a[0]; rhl7FlushResult($ares); if (!$procedure_report_id) { $procedure_report_id = rhl7FlushReport($arep); } $ares = array(); $ares['procedure_report_id'] = $procedure_report_id; // OBX-5 can be a very long string of text with "~" as line separators. // The first line of comments is reserved for such things. if (strlen($a[5]) > 200) { $ares['result_data_type'] = 'L'; $ares['result'] = ''; $ares['comments'] = rhl7Text($a[5]) . $commentdelim; } else { $ares['result_data_type'] = substr($a[2], 0, 1); // N, S or F $ares['result'] = rhl7Text($a[5]); $ares['comments'] = $commentdelim; } $tmp = explode($d2, $a[3]); $ares['result_code'] = rhl7Text($tmp[0]); $ares['result_text'] = rhl7Text($tmp[1]); $ares['date'] = rhl7DateTime($a[14]); $ares['facility'] = rhl7Text($a[15]); $ares['units'] = rhl7Text($a[6]); $ares['range'] = rhl7Text($a[7]); $ares['abnormal'] = rhl7Abnormal($a[8]); // values are lab dependent $ares['result_status'] = rhl7ReportStatus($a[11]); } else { if ($a[0] == 'NTE' && $context == 'OBX') { $ares['comments'] .= rhl7Text($a[3]) . $commentdelim; } else { return xl('Segment name') . " '{$a[0]}' " . xl('is misplaced or unknown'); } } } } } } } } } } rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); return ''; }
/** * 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; // 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; if (substr($hl7, 0, 3) != 'MSH') { return rhl7LogMsg(xl('Input does not begin with a MSH segment'), true); } // End-of-line delimiter for text in procedure_result.comments // Ensoftek: Different labs seem to send different EOLs. Edit HL7 input to a character we know. $commentdelim = "\r"; $hl7 = (string) str_replace(array("\r\n", "\r", "\n"), $commentdelim, $hl7); $today = time(); $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; $procedure_report_id = 0; $arep = array(); // holding area for OBR and its NTE data $ares = array(); // holding area for OBX and its NTE data $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 = $commentdelim; // Ensoftek: Convert all EOLs to \r, so we don't have to deal with inconsistencies. $d1 = substr($hl7, 3, 1); // typically | $d2 = substr($hl7, 4, 1); // typically ^ $d3 = substr($hl7, 5, 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; } // echo "<!-- $dryrun $seg -->\n"; // debugging ++$rhl7_segnum; $a = explode($d1, $seg); if ($a[0] == 'MSH') { // The following 2 cases happen only for multiple MSH segments in the same file. // But that probably shouldn't happen anyway? if ('ORU' == $msgtype && !$dryrun) { rhl7FlushResult($ares); rhl7FlushReport($arep); $ares = array(); $arep = 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'; } else { if ($a[8] == 'MDM^T02' || $a[8] == 'MDM^T04' || $a[8] == 'MDM^T08') { $msgtype = 'MDM'; $mdm_datetime = ''; $mdm_docname = ''; $mdm_text = ''; } else { return rhl7LogMsg(xl('MSH.8 message type is not supported') . ": '" . $a[8] . "'", true); } } $in_message_id = $a[9]; } else { if ($a[0] == 'PID') { $context = $a[0]; if ('ORU' == $msgtype && !$dryrun) { // Note these calls do nothing if the passed array is empty. rhl7FlushResult($ares); rhl7FlushReport($arep); } 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); } } $ares = array(); $arep = array(); $porow = false; $pcrow = false; $oprow = false; $in_orderid = 0; $in_ssn = preg_replace('/[^0-9]/', '', $a[4]); $in_dob = rhl7Date($a[7]); $tmp = explode($d2, $a[5]); $in_lname = rhl7Text($tmp[0]); $in_fname = rhl7Text($tmp[1]); $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_ss), 'fname' => strtoupper($in_fname), 'lname' => strtoupper($in_lname), 'DOB' => strtoupper($in_dob)); $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 } else { if ('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]); } } else { if ('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])); } } } } else { if ('ORC' == $a[0] && 'ORU' == $msgtype) { $context = $a[0]; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); // Next line will do something only if there was a report with no results. if (!$dryrun) { rhl7FlushReport($arep); } $arep = array(); $porow = false; $pcrow = false; if ($direction != 'R' && $a[2]) { $in_orderid = intval($a[2]); } } else { if ('TXA' == $a[0] && 'MDM' == $msgtype) { $context = $a[0]; $mdm_datetime = rhl7DateTime($a[4]); $mdm_docname = rhl7Text($a[12]); } else { if ($a[0] == 'NTE' && ($context == 'ORC' || $context == 'TXA')) { // Is this ever used? } else { if ('OBR' == $a[0] && 'ORU' == $msgtype) { $context = $a[0]; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); // Next line will do something only if there was a report with no results. if (!$dryrun) { rhl7FlushReport($arep); } $arep = array(); $procedure_report_id = 0; 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 = $tmp[1]; $in_report_status = rhl7ReportStatus($a[25]); // if ($direction == 'R') { // $in_orderid will be 0 here. // Save their order ID to procedure_order.control_id. // That column will need to change from bigint to varchar. // 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($a[2]) ? $a[3] : $a[2]; $porow = false; if ($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']); } else { // 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; // 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 = intval($oprow['id']); } } if (!$dryrun) { // 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 = ?", array($datetime_report, $provider_id, $lab_id, rhl7DateTime($a[22]), rhl7DateTime($a[7]), $patient_id, $encounter_id, $external_order_id)); // If an encounter was identified then link the order to it. if ($encounter_id && $in_orderid) { addForm($encounter_id, "Procedure Order", $in_orderid, "procedure_order", $patient_id); } } } // 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'])) { 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) { sqlInsert("INSERT INTO procedure_order_code SET " . "procedure_order_id = ?, " . "procedure_code = ?, " . "procedure_name = ?, " . "procedure_source = '2'", array($in_orderid, $in_procedure_code, $in_procedure_name)); $pcrow = sqlQuery($pcquery, $pcqueryargs); } else { // Dry run, make a dummy procedure_order_code row. $pcrow = array('procedure_order_id' => $in_orderid, 'procedure_order_seq' => 0); } } $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_report'] = rhl7Date($a[22]); $arep['report_status'] = $in_report_status; $arep['report_notes'] = ''; } else { if ($a[0] == 'NTE' && $context == 'OBR') { $arep['report_notes'] .= rhl7Text($a[3]) . "\n"; } else { if ('OBX' == $a[0] && 'ORU' == $msgtype) { $context = $a[0]; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); if (!$procedure_report_id) { if (!$dryrun) { $procedure_report_id = rhl7FlushReport($arep); } $arep = array(); } $ares['procedure_report_id'] = $procedure_report_id; $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[0]); $filename = date("Ymd_His") . '.' . $fileext; $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, $filename, rhl7MimeType($fileext), $data); if ($rc) { return rhl7LogMsg($rc); } $ares['document_id'] = $d->get_id(); } } else { if (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'] = ''; $ares['comments'] = rhl7Text($a[5]) . $commentdelim; } else { $ares['result'] = rhl7Text($a[5]); } } $tmp = explode($d2, $a[3]); $ares['result_code'] = rhl7Text($tmp[0]); $ares['result_text'] = rhl7Text($tmp[1]); $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. $performingOrganization = getPerformingOrganizationDetails($a[23], $a[24], $a[25], $d2, $commentdelim); if (isset($performingOrganization)) { $ares['facility'] .= $performingOrganization . $commentdelim; } } else { if ('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]); } } else { if ('ZEF' == $a[0] && 'ORU' == $msgtype) { // ZEF segment is treated like an OBX with an embedded Base64-encoded PDF. $context = 'OBX'; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); if (!$procedure_report_id) { if (!$dryrun) { $procedure_report_id = rhl7FlushReport($arep); } $arep = array(); } $ares['procedure_report_id'] = $procedure_report_id; $ares['result_data_type'] = 'E'; $ares['comments'] = $commentdelim; // $fileext = 'pdf'; $filename = date("Ymd_His") . '.' . $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, $filename, rhl7MimeType($fileext), $data); if ($rc) { return rhl7LogMsg($rc); } $ares['document_id'] = $d->get_id(); } $ares['date'] = $arep['date_report']; } else { if ('NTE' == $a[0] && 'OBX' == $context && 'ORU' == $msgtype) { $ares['comments'] .= rhl7Text($a[3]) . $commentdelim; } else { if ('SPM' == $a[0] && 'ORU' == $msgtype) { if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); if (!$procedure_report_id) { if (!$dryrun) { $procedure_report_id = rhl7FlushReport($arep); } $arep = array(); } rhl7UpdateReportWithSpecimen($a, $procedure_report_id, $d2); } else { if ('TQ1' == $a[0] && 'ORU' == $msgtype) { // Ignore and do nothing. } else { return rhl7LogMsg(xl('Segment name') . " '{$a[0]}' " . xl('is misplaced or unknown')); } } } } } } } } } } } } } } } if ('ORU' == $msgtype && !$dryrun) { rhl7FlushResult($ares); // Next line does something only for a report with no results. rhl7FlushReport($arep); } 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); } } return $rhl7_return; }
/** * Parse and save. * * @param string &$hl7 The input HL7 text. * @param char $direction B=Bidirectional, R=Results-only * @param book $dryrun True = do not update anything, just report errors * @return array Array of errors and match requests, if any. */ function receive_hl7_results(&$hl7, $lab_id = 0, $direction = 'B', $dryrun = false, $matchresp = NULL) { // This will hold returned error messages and related variables. $rhl7_return = array(); $rhl7_return['mssgs'] = array(); $rhl7_return['match'] = array(); $rhl7_segnum = 0; if (substr($hl7, 0, 3) != 'MSH') { return rhl7LogMsg(xl('Input does not begin with a MSH segment'), true); } // End-of-line delimiter for text in procedure_result.comments $commentdelim = "\n"; $today = time(); $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; $procedure_report_id = 0; $arep = array(); // holding area for OBR and its NTE data $ares = array(); // holding area for OBX and its NTE data $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 = ''; // Delimiters $d0 = "\r"; $d1 = substr($hl7, 3, 1); // typically | $d2 = substr($hl7, 4, 1); // typically ^ $d3 = substr($hl7, 5, 1); // typically ~ // We'll need the document category ID 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']; } $segs = explode($d0, $hl7); foreach ($segs as $seg) { if (empty($seg)) { continue; } // echo "<!-- $dryrun $seg -->\n"; // debugging ++$rhl7_segnum; $a = explode($d1, $seg); if ($a[0] == 'MSH') { $context = $a[0]; if ($a[8] != 'ORU^R01') { return rhl7LogMsg(xl('MSH.8 message type is not valid') . ": '" . $a[8] . "'", true); } $in_message_id = $a[9]; } else { if ($a[0] == 'PID') { $context = $a[0]; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); // Next line will do something only if there was a report with no results. if (!$dryrun) { rhl7FlushReport($arep); } $arep = array(); $porow = false; $pcrow = false; $in_orderid = 0; $in_ssn = preg_replace('/[^0-9]/', '', $a[4]); $in_dob = rhl7Date($a[7]); $tmp = explode($d2, $a[5]); $in_lname = rhl7Text($tmp[0]); $in_fname = rhl7Text($tmp[1]); $patient_id = 0; if ($direction == 'R') { $patient_id = match_patient($in_ss, $in_fname, $in_lname, $in_dob); if ($patient_id == -1) { // Indeterminate, check if the user has specified the patient. if (isset($matchresp[$rhl7_segnum])) { // This will be an existing pid, or 0 to specify creating a patient. $patient_id = intval($matchresp[$rhl7_segnum]); } else { // Nope, ask the user to do so. $rhl7_return['match'][$rhl7_segnum] = array('ss' => $in_ss, 'fname' => $in_fname, 'lname' => $in_lname, 'DOB' => $in_dob); } } if ($patient_id == 0 && !$dryrun) { // We must create the patient. $patient_id = create_skeleton_patient(array('fname' => $in_fname, 'lname' => $in_lname, 'DOB' => $in_dob, 'ss' => $in_ssn)); } if ($patient_id == -1) { $patient_id = 0; } } // end results-only logic } else { if ($a[0] == 'PV1') { // Save placer encounter number if present. if ($direction != 'R' && !empty($a[19])) { $tmp = explode($d2, $a[19]); $in_encounter = intval($tmp[0]); } } else { if ($a[0] == 'ORC') { $context = $a[0]; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); // Next line will do something only if there was a report with no results. if (!$dryrun) { rhl7FlushReport($arep); } $arep = array(); $porow = false; $pcrow = false; if ($direction != 'R' && $a[2]) { $in_orderid = intval($a[2]); } } else { if ($a[0] == 'NTE' && $context == 'ORC') { // Is this ever used? } else { if ($a[0] == 'OBR') { $context = $a[0]; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); // Next line will do something only if there was a report with no results. if (!$dryrun) { rhl7FlushReport($arep); } $arep = array(); $procedure_report_id = 0; 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 = $tmp[1]; $in_report_status = rhl7ReportStatus($a[25]); // if ($direction == 'R') { // $in_orderid will be 0 here. // Save their order ID to procedure_order.control_id. // That column will need to change from bigint to varchar. // 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($a[2]) ? $a[3] : $a[2]; $porow = false; if ($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']); } else { // 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; // 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. $op_lname = $op_fname = ''; $tmp = explode($d2, $a[16]); $op_npi = preg_replace('/[^0-9]/', '', $tmp[0]); if (!empty($tmp[1])) { $op_lname = $tmp[1]; } if (!empty($tmp[2])) { $op_fname = $tmp[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 FROM users WHERE {$where} " . "ORDER BY active DESC, authorized DESC, username DESC, id LIMIT 1", $qarr); if (!empty($oprow)) { $provider_id = intval($oprow['id']); } } if (!$dryrun) { // 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 = ?", array($datetime_report, $provider_id, $lab_id, rhl7DateTime($a[22]), rhl7DateTime($a[7]), $patient_id, $encounter_id, $external_order_id)); // If an encounter was identified then link the order to it. if ($encounter_id && $in_orderid) { addForm($encounter_id, "Procedure Order", $in_orderid, "procedure_order", $patient_id); } } } // 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'])) { 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) { sqlInsert("INSERT INTO procedure_order_code SET " . "procedure_order_id = ?, " . "procedure_code = ?, " . "procedure_name = ?, " . "procedure_source = '2'", array($in_orderid, $in_procedure_code, $in_procedure_name)); $pcrow = sqlQuery($pcquery, $pcqueryargs); } else { // Dry run, make a dummy procedure_order_code row. $pcrow = array('procedure_order_id' => $in_orderid, 'procedure_order_seq' => 0); } } $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_report'] = rhl7Date($a[22]); $arep['report_status'] = $in_report_status; $arep['report_notes'] = ''; } else { if ($a[0] == 'NTE' && $context == 'OBR') { $arep['report_notes'] .= rhl7Text($a[3]) . "\n"; } else { if ($a[0] == 'OBX') { $context = $a[0]; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); if (!$procedure_report_id) { if (!$dryrun) { $procedure_report_id = rhl7FlushReport($arep); } $arep = array(); } $ares['procedure_report_id'] = $procedure_report_id; $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[0]); $filename = date("Ymd_His") . '.' . $fileext; $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, $filename, rhl7MimeType($fileext), $data); if ($rc) { return rhl7LogMsg($rc); } $ares['document_id'] = $d->get_id(); } } else { if (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'] = ''; $ares['comments'] = rhl7Text($a[5]) . $commentdelim; } else { $ares['result'] = rhl7Text($a[5]); } } $tmp = explode($d2, $a[3]); $ares['result_code'] = rhl7Text($tmp[0]); $ares['result_text'] = rhl7Text($tmp[1]); $ares['date'] = rhl7DateTime($a[14]); $ares['facility'] = rhl7Text($a[15]); $ares['units'] = rhl7Text($a[6]); $ares['range'] = rhl7Text($a[7]); $ares['abnormal'] = rhl7Abnormal($a[8]); // values are lab dependent $ares['result_status'] = rhl7ReportStatus($a[11]); } else { if ($a[0] == 'ZEF') { // ZEF segment is treated like an OBX with an embedded Base64-encoded PDF. $context = 'OBX'; if (!$dryrun) { rhl7FlushResult($ares); } $ares = array(); if (!$procedure_report_id) { if (!$dryrun) { $procedure_report_id = rhl7FlushReport($arep); } $arep = array(); } $ares['procedure_report_id'] = $procedure_report_id; $ares['result_data_type'] = 'E'; $ares['comments'] = $commentdelim; // $fileext = 'pdf'; $filename = date("Ymd_His") . '.' . $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, $filename, rhl7MimeType($fileext), $data); if ($rc) { return rhl7LogMsg($rc); } $ares['document_id'] = $d->get_id(); } $ares['date'] = $arep['date_report']; } else { if ($a[0] == 'NTE' && $context == 'OBX') { $ares['comments'] .= rhl7Text($a[3]) . $commentdelim; } else { return rhl7LogMsg(xl('Segment name') . " '{$a[0]}' " . xl('is misplaced or unknown')); } } } } } } } } } } } if (!$dryrun) { rhl7FlushResult($ares); } // Next line will do something only if there was a report with no results. if (!$dryrun) { rhl7FlushReport($arep); } return $rhl7_return; }
/** * Parse and save. * * @param string &$pprow A row from the procedure_providers table. * @param string &$hl7 The input HL7 text. * @return string Error text, or empty if no errors. */ function receive_hl7_results(&$hl7) { if (substr($hl7, 0, 3) != 'MSH') { return xl('Input does not begin with a MSH segment'); } // End-of-line delimiter for text in procedure_result.comments $commentdelim = "\n"; $today = time(); $in_message_id = ''; $in_ssn = ''; $in_dob = ''; $in_lname = ''; $in_fname = ''; $in_orderid = 0; $in_procedure_code = ''; $in_report_status = ''; $in_encounter = 0; $porow = false; $pcrow = false; $procedure_report_id = 0; $arep = array(); // holding area for OBR and its NTE data $ares = array(); // holding area for OBX and its NTE data $code_seq_array = array(); // tracks sequence numbers of order codes // This is so we know where we are if a segment like NTE that can appear in // different places is encountered. $context = ''; // Delimiters $d0 = "\r"; $d1 = substr($hl7, 3, 1); // typically | $d2 = substr($hl7, 4, 1); // typically ^ $d3 = substr($hl7, 5, 1); // typically ~ // We'll need the document category ID for any embedded documents. $catrow = sqlQuery("SELECT id FROM categories WHERE name = ?", array($GLOBALS['lab_results_category_name'])); if (empty($catrow['id'])) { return xl('Document category for lab results does not exist') . ': ' . $GLOBALS['lab_results_category_name']; } $results_category_id = $catrow['id']; $segs = explode($d0, $hl7); foreach ($segs as $seg) { if (empty($seg)) { continue; } $a = explode($d1, $seg); if ($a[0] == 'MSH') { $context = $a[0]; if ($a[8] != 'ORU^R01') { return xl('Message type') . " '{$a[8]}' " . xl('does not seem valid'); } $in_message_id = $a[9]; } else { if ($a[0] == 'PID') { $context = $a[0]; rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); $in_ssn = $a[4]; $in_dob = $a[7]; // yyyymmdd format $tmp = explode($d2, $a[5]); $in_lname = $tmp[0]; $in_fname = $tmp[1]; } else { if ($a[0] == 'PV1') { // Save placer encounter number if present. if (!empty($a[19])) { $tmp = explode($d2, $a[19]); $in_encounter = intval($tmp[0]); } } else { if ($a[0] == 'ORC') { $context = $a[0]; rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); $porow = false; $pcrow = false; if ($a[2]) { $in_orderid = intval($a[2]); } } else { if ($a[0] == 'NTE' && $context == 'ORC') { // TBD? Is this ever used? } else { if ($a[0] == 'OBR') { $context = $a[0]; rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); $procedure_report_id = 0; if ($a[2]) { $in_orderid = intval($a[2]); } $tmp = explode($d2, $a[4]); $in_procedure_code = $tmp[0]; $in_procedure_name = $tmp[1]; $in_report_status = rhl7ReportStatus($a[25]); 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)) { return xl('Procedure order') . " '{$in_orderid}' " . xl('was not found'); } if ($in_encounter) { if ($porow['encounter_id'] != $in_encounter) { return 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. } $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"; $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code, $code_seq_array['$in_procedure_code'])); 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. sqlInsert("INSERT INTO procedure_order_code SET " . "procedure_order_id = ?, " . "procedure_code = ?, " . "procedure_name = ?, " . "procedure_source = '2'", array($in_orderid, $in_procedure_code, $in_procedure_name)); $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code)); } $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_report'] = substr(rhl7DateTime($a[22]), 0, 10); $arep['report_status'] = $in_report_status; $arep['report_notes'] = ''; } else { if ($a[0] == 'NTE' && $context == 'OBR') { $arep['report_notes'] .= rhl7Text($a[3]) . "\n"; } else { if ($a[0] == 'OBX') { $context = $a[0]; rhl7FlushResult($ares); if (!$procedure_report_id) { $procedure_report_id = rhl7FlushReport($arep); } $ares = array(); $ares['procedure_report_id'] = $procedure_report_id; $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[0]); $filename = date("Ymd_His") . '.' . $fileext; $data = rhl7DecodeData($tmp[3], &$tmp[4]); if ($data === FALSE) { return xl('Invalid encapsulated data encoding type') . ': ' . $tmp[3]; } $d = new Document(); $rc = $d->createDocument($porow['patient_id'], $results_category_id, $filename, rhl7MimeType($fileext), $data); if ($rc) { return $rc; } // This would be error message text. $ares['document_id'] = $d->get_id(); } else { if (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'] = ''; $ares['comments'] = rhl7Text($a[5]) . $commentdelim; } else { $ares['result'] = rhl7Text($a[5]); } } $tmp = explode($d2, $a[3]); $ares['result_code'] = rhl7Text($tmp[0]); $ares['result_text'] = rhl7Text($tmp[1]); $ares['date'] = rhl7DateTime($a[14]); $ares['facility'] = rhl7Text($a[15]); $ares['units'] = rhl7Text($a[6]); $ares['range'] = rhl7Text($a[7]); $ares['abnormal'] = rhl7Abnormal($a[8]); // values are lab dependent $ares['result_status'] = rhl7ReportStatus($a[11]); } else { if ($a[0] == 'ZEF') { // ZEF segment is treated like an OBX with an embedded Base64-encoded PDF. $context = 'OBX'; rhl7FlushResult($ares); if (!$procedure_report_id) { $procedure_report_id = rhl7FlushReport($arep); } $ares = array(); $ares['procedure_report_id'] = $procedure_report_id; $ares['result_data_type'] = 'E'; $ares['comments'] = $commentdelim; // $fileext = 'pdf'; $filename = date("Ymd_His") . '.' . $fileext; $data = rhl7DecodeData('Base64', $a[2]); if ($data === FALSE) { return xl('ZEF segment internal error'); } $d = new Document(); $rc = $d->createDocument($porow['patient_id'], $results_category_id, $filename, rhl7MimeType($fileext), $data); if ($rc) { return $rc; } // This would be error message text. $ares['document_id'] = $d->get_id(); // $ares['date'] = $arep['date_report']; } else { if ($a[0] == 'NTE' && $context == 'OBX') { $ares['comments'] .= rhl7Text($a[3]) . $commentdelim; } else { return xl('Segment name') . " '{$a[0]}' " . xl('is misplaced or unknown'); } } } } } } } } } } } rhl7FlushResult($ares); // Next line will do something only if there was a report with no results. rhl7FlushReport($arep); return ''; }