Ejemplo n.º 1
0
/**
 * 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);
    }
    // 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_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;
        }
        // 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';
            } 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 ('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]);
                $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_ss), 'fname' => strtoupper($in_fname), 'lname' => strtoupper($in_lname), 'mname' => strtoupper($in_mname), '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 ('PD1' == $a[0]) {
                    // TBD: Save primary care provider name ($a[4]) somewhere?
                } 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];
                            $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];
                                        $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 = $tmp[1];
                                        $in_report_status = rhl7ReportStatus($a[25]);
                                        // 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($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;
                                                // 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) {
                                                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_source = '2'", array($in_orderid, $procedure_order_seq['increment'], $in_procedure_code, $in_procedure_name));
                                                $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);
                                            }
                                        }
                                        $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();
                                    } else {
                                        if ($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";
                                        } else {
                                            if ('OBX' == $a[0] && 'ORU' == $msgtype) {
                                                $tmp = explode($d2, $a[3]);
                                                $result_code = rhl7Text($tmp[0]);
                                                $result_text = rhl7Text($tmp[1]);
                                                // 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']) - 1) . '~' . 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[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 ($a[2] == 'CWE') {
                                                        $ares['result'] = rhl7CWE($a[5], $d2);
                                                    } else {
                                                        if ($a[2] == 'SN') {
                                                            $ares['result'] = trim(str_replace($d2, ' ', $a[5]));
                                                        } else {
                                                            if ($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'] = '';
                                                                $ares['comments'] = rhl7Text($a[5]) . $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.
                                                $performingOrganization = getPerformingOrganizationDetails($a[23], $a[24], $a[25], $d2, $commentdelim);
                                                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;
                                            } 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';
                                                        $ares = array();
                                                        $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'];
                                                        // $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;
                                                    } else {
                                                        if ('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;
                                                        } else {
                                                            if ('SPM' == $a[0] && 'ORU' == $msgtype) {
                                                                rhl7UpdateReportWithSpecimen($amain, $a, $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'));
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    // 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;
}
Ejemplo n.º 2
0
/**
 * 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
  $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;
  $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 = "\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 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];
      if ($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, // TBD?
          );
        }
      }
      $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, // TBD: Make sure not 0
            $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 ('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, // 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'];
    }

    else if ('NTE' == $a[0] && 'OBX' == $context && 'ORU' == $msgtype) {
      $ares['comments'] .= rhl7Text($a[3]) . $commentdelim;
    }

    // Add code here for any other segment types that may be present.

    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;
}