Changes to support a results-only lab interface.
[openemr.git] / interface / orders / receive_hl7_results.inc.php
blob9b0ecb9efe9ec0f47362a7acbcfc2d8d75f06500
1 <?php
2 /**
3 * Functions to support parsing and saving hl7 results.
5 * Copyright (C) 2013-2014 Rod Roark <rod@sunsetsystems.com>
7 * LICENSE: This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>.
18 * @package OpenEMR
19 * @author Rod Roark <rod@sunsetsystems.com>
22 $rhl7_return = array();
23 $rhl7_segnum = 0;
25 function rhl7LogMsg($msg, $fatal=true) {
26 // global $rhl7_return, $rhl7_segnum;
27 $rhl7_return['mssgs'][] = $msg;
28 if ($fatal) {
29 $rhl7_return['fatal'] = true;
30 newEvent("lab-results-error", $_SESSION['authUser'], $_SESSION['authProvider'], 0, $msg);
32 return $rhl7_return;
35 function rhl7InsertRow(&$arr, $tablename) {
36 if (empty($arr)) return;
38 // echo "<!-- "; // debugging
39 // print_r($arr);
40 // echo " -->\n";
42 $query = "INSERT INTO $tablename SET";
43 $binds = array();
44 $sep = '';
45 foreach ($arr as $key => $value) {
46 $query .= "$sep `$key` = ?";
47 $sep = ',';
48 $binds[] = $value;
50 $arr = array();
51 return sqlInsert($query, $binds);
54 function rhl7FlushResult(&$ares) {
55 return rhl7InsertRow($ares, 'procedure_result');
58 function rhl7FlushReport(&$arep) {
59 return rhl7InsertRow($arep, 'procedure_report');
62 function rhl7Text($s) {
63 $s = str_replace('\\S\\' ,'^' , $s);
64 $s = str_replace('\\F\\' ,'|' , $s);
65 $s = str_replace('\\R\\' ,'~' , $s);
66 $s = str_replace('\\T\\' ,'&' , $s);
67 $s = str_replace('\\X0d\\',"\r", $s);
68 $s = str_replace('\\E\\' ,'\\', $s);
69 return $s;
72 function rhl7DateTime($s) {
73 $s = preg_replace('/[^0-9]/', '', $s);
74 if (empty($s)) return '0000-00-00 00:00:00';
75 $ret = substr($s, 0, 4) . '-' . substr($s, 4, 2) . '-' . substr($s, 6, 2);
76 if (strlen($s) > 8) $ret .= ' ' . substr($s, 8, 2) . ':' . substr($s, 10, 2) . ':';
77 if (strlen($s) > 12) {
78 $ret .= substr($s, 12, 2);
79 } else {
80 $ret .= '00';
82 return $ret;
85 function rhl7Date($s) {
86 return substr(rhl7DateTime($s), 0, 10);
89 function rhl7Abnormal($s) {
90 if ($s == '' ) return 'no';
91 if ($s == 'N' ) return 'no';
92 if ($s == 'A' ) return 'yes';
93 if ($s == 'H' ) return 'high';
94 if ($s == 'L' ) return 'low';
95 if ($s == 'HH') return 'vhigh';
96 if ($s == 'LL') return 'vlow';
97 return rhl7Text($s);
100 function rhl7ReportStatus($s) {
101 if ($s == 'F') return 'final';
102 if ($s == 'P') return 'prelim';
103 if ($s == 'C') return 'correct';
104 return rhl7Text($s);
108 * Convert a lower case file extension to a MIME type.
109 * The extension comes from OBX[5][0] which is itself a huge assumption that
110 * the HL7 2.3 standard does not help with. Don't be surprised when we have to
111 * adapt to conventions of various other labs.
113 * @param string $fileext The lower case extension.
114 * @return string MIME type.
116 function rhl7MimeType($fileext) {
117 if ($fileext == 'pdf') return 'application/pdf';
118 if ($fileext == 'doc') return 'application/msword';
119 if ($fileext == 'rtf') return 'application/rtf';
120 if ($fileext == 'txt') return 'text/plain';
121 if ($fileext == 'zip') return 'application/zip';
122 return 'application/octet-stream';
126 * Extract encapsulated document data according to its encoding type.
128 * @param string $enctype Encoding type from OBX[5][3].
129 * @param string &$src Encoded data from OBX[5][4].
130 * @return string Decoded data, or FALSE if error.
132 function rhl7DecodeData($enctype, &$src) {
133 if ($enctype == 'Base64') return base64_decode($src);
134 if ($enctype == 'A' ) return rhl7Text($src);
135 if ($enctype == 'Hex') {
136 $data = '';
137 for ($i = 0; $i < strlen($src) - 1; $i += 2) {
138 $data .= chr(hexdec($src[$i] . $src[$i+1]));
140 return $data;
142 return FALSE;
146 * Look for a patient matching the given data.
147 * Return values are:
148 * >0 Definite match, this is the pid.
149 * 0 No patient is close to a match.
150 * -1 It's not clear if there is a match.
152 function match_patient($in_ss, $in_fname, $in_lname, $in_dob) {
153 $patient_id = 0;
154 $tmp = sqlQuery("SELECT pid FROM patient_data WHERE " .
155 "((ss IS NULL OR ss = '' OR '' = ?) AND " .
156 "fname IS NOT NULL AND fname != '' AND fname = ? AND " .
157 "lname IS NOT NULL AND lname != '' AND lname = ? AND " .
158 "DOB IS NOT NULL AND DOB = ?) OR " .
159 "(ss IS NOT NULL AND ss != '' AND ss = ? AND (" .
160 "fname IS NOT NULL AND fname != '' AND fname = ? OR " .
161 "lname IS NOT NULL AND lname != '' AND lname = ? OR " .
162 "DOB IS NOT NULL AND DOB = ?)) " .
163 "ORDER BY ss DESC, pid DESC LIMIT 1",
164 array($in_ss, $in_fname, $in_lname, $in_dob, $in_ss, $in_fname, $in_lname, $in_dob));
165 if (!empty($tmp['pid'])) {
166 // Got a match.
167 $patient_id = intval($tmp['pid']);
169 else {
170 // No match good enough, figure out if there's enough ambiguity to ask the user.
171 $tmp = sqlQuery("SELECT pid FROM patient_data WHERE " .
172 "(ss IS NOT NULL AND ss != '' AND ss = ?) OR " .
173 "(fname IS NOT NULL AND fname != '' AND fname = ? AND " .
174 "lname IS NOT NULL AND lname != '' AND lname = ?) OR " .
175 "(DOB IS NOT NULL AND DOB = ?) " .
176 "LIMIT 1",
177 array($in_ss, $in_fname, $in_lname, $in_dob));
178 if (!empty($tmp['pid'])) {
179 $patient_id = -1;
182 return $patient_id;
186 * Create a patient using whatever patient_data attributes are provided.
188 function create_skeleton_patient($patient_data) {
189 $employer_data = array();
190 $tmp = sqlQuery("SELECT MAX(pid)+1 AS pid FROM patient_data");
191 $ptid = empty($tmp['pid']) ? 1 : intval($tmp['pid']);
192 if (!isset($patient_data['pubpid'])) $patient_data['pubpid'] = $ptid;
193 updatePatientData($ptid, $patient_data, true);
194 updateEmployerData($ptid, $employer_data, true);
195 newHistoryData($ptid);
196 return $ptid;
200 * Parse and save.
202 * @param string &$hl7 The input HL7 text.
203 * @param char $direction B=Bidirectional, R=Results-only
204 * @param book $dryrun True = do not update anything, just report errors
205 * @return array Array of errors and match requests, if any.
207 function receive_hl7_results(&$hl7, $lab_id=0, $direction='B', $dryrun=false, $matchresp=NULL) {
208 // This will hold returned error messages and related variables.
209 $rhl7_return = array();
210 $rhl7_return['mssgs'] = array();
211 $rhl7_return['match'] = array();
212 $rhl7_segnum = 0;
214 if (substr($hl7, 0, 3) != 'MSH') {
215 return rhl7LogMsg(xl('Input does not begin with a MSH segment'), true);
218 // End-of-line delimiter for text in procedure_result.comments
219 $commentdelim = "\n";
221 $today = time();
223 $in_message_id = '';
224 $in_ssn = '';
225 $in_dob = '';
226 $in_lname = '';
227 $in_fname = '';
228 $in_orderid = 0;
229 $in_procedure_code = '';
230 $in_report_status = '';
231 $in_encounter = 0;
232 $patient_id = 0; // for results-only patient matching logic
234 $porow = false;
235 $pcrow = false;
236 $procedure_report_id = 0;
237 $arep = array(); // holding area for OBR and its NTE data
238 $ares = array(); // holding area for OBX and its NTE data
239 $code_seq_array = array(); // tracks sequence numbers of order codes
240 $results_category_id = 0; // document category ID for lab results
242 // This is so we know where we are if a segment like NTE that can appear in
243 // different places is encountered.
244 $context = '';
246 // Delimiters
247 $d0 = "\r";
248 $d1 = substr($hl7, 3, 1); // typically |
249 $d2 = substr($hl7, 4, 1); // typically ^
250 $d3 = substr($hl7, 5, 1); // typically ~
252 // We'll need the document category ID for any embedded documents.
253 $catrow = sqlQuery("SELECT id FROM categories WHERE name = ?",
254 array($GLOBALS['lab_results_category_name']));
255 if (empty($catrow['id'])) {
256 return rhl7LogMsg(xl('Document category for lab results does not exist') .
257 ': ' . $GLOBALS['lab_results_category_name'], true);
259 else {
260 $results_category_id = $catrow['id'];
263 $segs = explode($d0, $hl7);
265 foreach ($segs as $seg) {
266 if (empty($seg)) continue;
268 // echo "<!-- $dryrun $seg -->\n"; // debugging
270 ++$rhl7_segnum;
271 $a = explode($d1, $seg);
273 if ($a[0] == 'MSH') {
274 $context = $a[0];
275 if ($a[8] != 'ORU^R01') {
276 return rhl7LogMsg(xl('MSH.8 message type is not valid') . ": '" . $a[8] . "'", true);
278 $in_message_id = $a[9];
281 else if ($a[0] == 'PID') {
282 $context = $a[0];
283 if (!$dryrun) rhl7FlushResult($ares);
284 $ares = array();
285 // Next line will do something only if there was a report with no results.
286 if (!$dryrun) rhl7FlushReport($arep);
287 $arep = array();
288 $porow = false;
289 $pcrow = false;
290 $in_orderid = 0;
291 $in_ssn = preg_replace('/[^0-9]/', '', $a[4]);
292 $in_dob = rhl7Date($a[7]);
293 $tmp = explode($d2, $a[5]);
294 $in_lname = rhl7Text($tmp[0]);
295 $in_fname = rhl7Text($tmp[1]);
296 $patient_id = 0;
297 if ($direction == 'R') {
298 $patient_id = match_patient($in_ss, $in_fname, $in_lname, $in_dob);
299 if ($patient_id == -1) {
300 // Indeterminate, check if the user has specified the patient.
301 if (isset($matchresp[$rhl7_segnum]) /* && $matchresp[$rhl7_segnum] !== '' */) {
302 // This will be an existing pid, or 0 to specify creating a patient.
303 $patient_id = intval($matchresp[$rhl7_segnum]);
305 else {
306 // Nope, ask the user to do so.
307 $rhl7_return['match'][$rhl7_segnum] = array('ss' => $in_ss,
308 'fname' => $in_fname, 'lname' => $in_lname, 'DOB' => $in_dob);
311 if ($patient_id == 0 && !$dryrun) {
312 // We must create the patient.
313 $patient_id = create_skeleton_patient(array(
314 'fname' => $in_fname,
315 'lname' => $in_lname,
316 'DOB' => $in_dob,
317 'ss' => $in_ssn,
320 if ($patient_id == -1) $patient_id = 0;
321 } // end results-only logic
324 else if ($a[0] == 'PV1') {
325 // Save placer encounter number if present.
326 if ($direction != 'R' && !empty($a[19])) {
327 $tmp = explode($d2, $a[19]);
328 $in_encounter = intval($tmp[0]);
332 else if ($a[0] == 'ORC') {
333 $context = $a[0];
334 if (!$dryrun) rhl7FlushResult($ares);
335 $ares = array();
336 // Next line will do something only if there was a report with no results.
337 if (!$dryrun) rhl7FlushReport($arep);
338 $arep = array();
339 $porow = false;
340 $pcrow = false;
341 if ($direction != 'R' && $a[2]) $in_orderid = intval($a[2]);
344 else if ($a[0] == 'NTE' && $context == 'ORC') {
345 // Is this ever used?
348 else if ($a[0] == 'OBR') {
349 $context = $a[0];
350 if (!$dryrun) rhl7FlushResult($ares);
351 $ares = array();
352 // Next line will do something only if there was a report with no results.
353 if (!$dryrun) rhl7FlushReport($arep);
354 $arep = array();
355 $procedure_report_id = 0;
356 if ($direction != 'R' && $a[2]) {
357 $in_orderid = intval($a[2]);
358 $porow = false;
359 $pcrow = false;
361 $tmp = explode($d2, $a[4]);
362 $in_procedure_code = $tmp[0];
363 $in_procedure_name = $tmp[1];
364 $in_report_status = rhl7ReportStatus($a[25]);
366 if ($direction == 'R') {
367 // $in_orderid will be 0 here.
368 // Save their order ID to procedure_order.control_id.
369 // That column will need to change from bigint to varchar.
370 // Look for an existing order using that plus lab_id.
371 // Ordering provider is OBR.16 (NPI^Last^First).
372 // Might not need to create a dummy encounter.
373 // Need also provider_id (probably), patient_id, date_ordered, lab_id.
374 // We have observation date/time in OBR.7.
375 // We have report date/time in OBR.22.
376 // We do not have an order date.
378 $external_order_id = empty($a[2]) ? $a[3] : $a[2];
379 $porow = false;
380 if ($external_order_id) {
381 $porow = sqlQuery("SELECT * FROM procedure_order " .
382 "WHERE lab_id = ? AND control_id = ? " .
383 "ORDER BY procedure_order_id DESC LIMIT 1",
384 array($lab_id, $external_order_id));
386 if (!empty($porow)) {
387 $in_orderid = intval($porow['procedure_order_id']);
389 else {
390 // Create order.
391 // Need to identify the ordering provider and, if possible, a recent encounter.
392 $datetime_report = rhl7DateTime($a[22]);
393 $date_report = substr($datetime_report, 0, 10) . ' 00:00:00';
394 $encounter_id = 0;
395 $provider_id = 0;
396 // Look for the most recent encounter within 30 days of the report date.
397 $encrow = sqlQuery("SELECT encounter FROM form_encounter WHERE " .
398 "pid = ? AND date <= ? AND DATE_ADD(date, INTERVAL 30 DAY) > ? " .
399 "ORDER BY date DESC, encounter DESC LIMIT 1",
400 array($patient_id, $date_report, $date_report));
401 if (!empty($encrow)) {
402 $encounter_id = intval($encrow['encounter']);
403 $provider_id = intval($encrow['provider_id']);
405 if (!$provider_id) {
406 // Attempt ordering provider matching by name or NPI.
407 $op_lname = $op_fname = '';
408 $tmp = explode($d2, $a[16]);
409 $op_npi = preg_replace('/[^0-9]/', '', $tmp[0]);
410 if (!empty($tmp[1])) $op_lname = $tmp[1];
411 if (!empty($tmp[2])) $op_fname = $tmp[2];
412 if ($op_npi || ($op_fname && $op_lname)) {
413 if ($op_npi) {
414 if ($op_fname && $op_lname) {
415 $where = "(npi IS NOT NULL AND npi = ?) OR ((npi IS NULL OR npi = ?) AND lname = ? AND fname = ?)";
416 $qarr = array($op_npi, '', $op_lname, $op_fname);
418 else {
419 $where = "npi IS NOT NULL AND npi = ?";
420 $qarr = array($op_npi);
423 else {
424 $where = "lname = ? AND fname = ?";
425 $qarr = array($op_lname, $op_fname);
428 $oprow = sqlQuery("SELECT id FROM users WHERE $where " .
429 "ORDER BY active DESC, authorized DESC, username DESC, id LIMIT 1",
430 $qarr);
431 if (!empty($oprow)) $provider_id = intval($oprow['id']);
433 if (!$dryrun) {
434 // Now create the procedure order.
435 $in_orderid = sqlInsert("INSERT INTO procedure_order SET " .
436 "date_ordered = ?, " .
437 "provider_id = ?, " .
438 "lab_id = ?, " .
439 "date_collected = ?, " .
440 "date_transmitted = ?, " .
441 "patient_id = ?, " .
442 "encounter_id = ?, " .
443 "control_id = ?",
444 array($datetime_report, $provider_id, $lab_id, rhl7DateTime($a[22]),
445 rhl7DateTime($a[7]), $patient_id, $encounter_id, $external_order_id));
446 // If an encounter was identified then link the order to it.
447 if ($encounter_id && $in_orderid) {
448 addForm($encounter_id, "Procedure Order", $in_orderid, "procedure_order", $patient_id);
451 } // end no $porow
452 } // end results-only
453 if (empty($porow)) {
454 $porow = sqlQuery("SELECT * FROM procedure_order WHERE " .
455 "procedure_order_id = ?", array($in_orderid));
456 // The order must already exist. Currently we do not handle electronic
457 // results returned for manual orders.
458 if (empty($porow) && !($dryrun && $direction == 'R')) {
459 return rhl7LogMsg(xl('Procedure order not found') . ": $in_orderid", true);
461 if ($in_encounter) {
462 if ($direction != 'R' && $porow['encounter_id'] != $in_encounter) {
463 return rhl7LogMsg(xl('Encounter ID') .
464 " '" . $porow['encounter_id'] . "' " .
465 xl('for OBR placer order number') .
466 " '$in_orderid' " .
467 xl('does not match the PV1 encounter number') .
468 " '$in_encounter'");
471 else {
472 // They did not return an encounter number to verify, so more checking
473 // might be done here to make sure the patient seems to match.
475 // Save the lab's control ID if there is one.
476 $tmp = explode($d2, $a[3]);
477 $control_id = $tmp[0];
478 if ($control_id && empty($porow['control_id'])) {
479 sqlStatement("UPDATE procedure_order SET control_id = ? WHERE " .
480 "procedure_order_id = ?", array($control_id, $in_orderid));
482 $code_seq_array = array();
484 // Find the order line item (procedure code) that matches this result.
485 // If there is more than one, then we select the one whose sequence number
486 // is next after the last sequence number encountered for this procedure
487 // code; this assumes that result OBRs are returned in the same sequence
488 // as the corresponding OBRs in the order.
489 if (!isset($code_seq_array[$in_procedure_code])) {
490 $code_seq_array[$in_procedure_code] = 0;
492 $pcquery = "SELECT pc.* FROM procedure_order_code AS pc " .
493 "WHERE pc.procedure_order_id = ? AND pc.procedure_code = ? " .
494 "ORDER BY (procedure_order_seq <= ?), procedure_order_seq LIMIT 1";
495 $pcqueryargs = array($in_orderid, $in_procedure_code, $code_seq_array[$in_procedure_code]);
496 $pcrow = sqlQuery($pcquery, $pcqueryargs);
497 if (empty($pcrow)) {
498 // There is no matching procedure in the order, so it must have been
499 // added after the original order was sent, either as a manual request
500 // from the physician or as a "reflex" from the lab.
501 // procedure_source = '2' indicates this.
502 if (!$dryrun) {
503 sqlInsert("INSERT INTO procedure_order_code SET " .
504 "procedure_order_id = ?, " .
505 "procedure_code = ?, " .
506 "procedure_name = ?, " .
507 "procedure_source = '2'",
508 array($in_orderid, $in_procedure_code, $in_procedure_name));
509 $pcrow = sqlQuery($pcquery, $pcqueryargs);
511 else {
512 // Dry run, make a dummy procedure_order_code row.
513 $pcrow = array(
514 'procedure_order_id' => $in_orderid,
515 'procedure_order_seq' => 0, // TBD?
519 $code_seq_array[$in_procedure_code] = 0 + $pcrow['procedure_order_seq'];
520 $arep = array();
521 $arep['procedure_order_id'] = $in_orderid;
522 $arep['procedure_order_seq'] = $pcrow['procedure_order_seq'];
523 $arep['date_collected'] = rhl7DateTime($a[7]);
524 $arep['date_report'] = rhl7Date($a[22]);
525 $arep['report_status'] = $in_report_status;
526 $arep['report_notes'] = '';
529 else if ($a[0] == 'NTE' && $context == 'OBR') {
530 $arep['report_notes'] .= rhl7Text($a[3]) . "\n";
533 else if ($a[0] == 'OBX') {
534 $context = $a[0];
535 if (!$dryrun) rhl7FlushResult($ares);
536 $ares = array();
537 if (!$procedure_report_id) {
538 if (!$dryrun) $procedure_report_id = rhl7FlushReport($arep);
539 $arep = array();
541 $ares['procedure_report_id'] = $procedure_report_id;
542 $ares['result_data_type'] = substr($a[2], 0, 1); // N, S, F or E
543 $ares['comments'] = $commentdelim;
544 if ($a[2] == 'ED') {
545 // This is the case of results as an embedded document. We will create
546 // a normal patient document in the assigned category for lab results.
547 $tmp = explode($d2, $a[5]);
548 $fileext = strtolower($tmp[0]);
549 $filename = date("Ymd_His") . '.' . $fileext;
550 $data = rhl7DecodeData($tmp[3], $tmp[4]);
551 if ($data === FALSE) {
552 return rhl7LogMsg(xl('Invalid encapsulated data encoding type') . ': ' . $tmp[3]);
554 if (!$dryrun) {
555 $d = new Document();
556 $rc = $d->createDocument($porow['patient_id'], $results_category_id, // TBD: Make sure not 0
557 $filename, rhl7MimeType($fileext), $data);
558 if ($rc) return rhl7LogMsg($rc);
559 $ares['document_id'] = $d->get_id();
562 else if (strlen($a[5]) > 200) {
563 // OBX-5 can be a very long string of text with "~" as line separators.
564 // The first line of comments is reserved for such things.
565 $ares['result_data_type'] = 'L';
566 $ares['result'] = '';
567 $ares['comments'] = rhl7Text($a[5]) . $commentdelim;
569 else {
570 $ares['result'] = rhl7Text($a[5]);
572 $tmp = explode($d2, $a[3]);
573 $ares['result_code'] = rhl7Text($tmp[0]);
574 $ares['result_text'] = rhl7Text($tmp[1]);
575 $ares['date'] = rhl7DateTime($a[14]);
576 $ares['facility'] = rhl7Text($a[15]);
577 $ares['units'] = rhl7Text($a[6]);
578 $ares['range'] = rhl7Text($a[7]);
579 $ares['abnormal'] = rhl7Abnormal($a[8]); // values are lab dependent
580 $ares['result_status'] = rhl7ReportStatus($a[11]);
583 else if ($a[0] == 'ZEF') {
584 // ZEF segment is treated like an OBX with an embedded Base64-encoded PDF.
585 $context = 'OBX';
586 if (!$dryrun) rhl7FlushResult($ares);
587 $ares = array();
588 if (!$procedure_report_id) {
589 if (!$dryrun) $procedure_report_id = rhl7FlushReport($arep);
590 $arep = array();
592 $ares['procedure_report_id'] = $procedure_report_id;
593 $ares['result_data_type'] = 'E';
594 $ares['comments'] = $commentdelim;
596 $fileext = 'pdf';
597 $filename = date("Ymd_His") . '.' . $fileext;
598 $data = rhl7DecodeData('Base64', $a[2]);
599 if ($data === FALSE) return rhl7LogMsg(xl('ZEF segment internal error'));
600 if (!$dryrun) {
601 $d = new Document();
602 $rc = $d->createDocument($porow['patient_id'], $results_category_id, // TBD: Make sure not 0
603 $filename, rhl7MimeType($fileext), $data);
604 if ($rc) return rhl7LogMsg($rc);
605 $ares['document_id'] = $d->get_id();
607 $ares['date'] = $arep['date_report'];
610 else if ($a[0] == 'NTE' && $context == 'OBX') {
611 $ares['comments'] .= rhl7Text($a[3]) . $commentdelim;
614 // Add code here for any other segment types that may be present.
616 else {
617 return rhl7LogMsg(xl('Segment name') . " '${a[0]}' " . xl('is misplaced or unknown'));
621 if (!$dryrun) rhl7FlushResult($ares);
622 // Next line will do something only if there was a report with no results.
623 if (!$dryrun) rhl7FlushReport($arep);
624 return $rhl7_return;
628 * Poll all eligible labs for new results and store them in the database.
630 * @param array &$info Conveys information to and from the caller:
631 * FROM THE CALLER:
632 * $info["$ppid/$filename"]['delete'] = a non-empty value if file deletion is requested.
633 * $info["$ppid/$filename"]['select'] = array of patient matching responses where key is segment
634 * number and value is selected pid for this patient, or 0 to create the patient.
635 * TO THE CALLER:
636 * $info["$ppid/$filename"]['mssgs'] = array of messages from this function.
637 * $info["$ppid/$filename"]['match'] = array of patient matching requests where key is
638 * (PID) segment number and value is an associative array of patient attributes from the hl7 file:
639 * ss, fname, lname, DOB.
641 * @return string Error text, or empty if no errors.
643 function poll_hl7_results(&$info) {
644 global $srcdir;
646 // echo "<!-- post: "; print_r($_POST); echo " -->\n"; // debugging
647 // echo "<!-- in: "; print_r($info); echo " -->\n"; // debugging
649 $filecount = 0;
650 $badcount = 0;
652 $ppres = sqlStatement("SELECT * FROM procedure_providers ORDER BY name");
654 while ($pprow = sqlFetchArray($ppres)) {
655 $ppid = $pprow['ppid'];
656 $protocol = $pprow['protocol'];
657 $remote_host = $pprow['remote_host'];
658 $hl7 = '';
660 if ($protocol == 'SFTP') {
661 $remote_port = 22;
662 // Hostname may have ":port" appended to specify a nonstandard port number.
663 if ($i = strrpos($remote_host, ':')) {
664 $remote_port = 0 + substr($remote_host, $i + 1);
665 $remote_host = substr($remote_host, 0, $i);
667 ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . "$srcdir/phpseclib");
668 require_once("$srcdir/phpseclib/Net/SFTP.php");
669 // Compute the target path name.
670 $pathname = '.';
671 if ($pprow['results_path']) $pathname = $pprow['results_path'] . '/' . $pathname;
672 // Connect to the server and enumerate files to process.
673 $sftp = new Net_SFTP($remote_host, $remote_port);
674 if (!$sftp->login($pprow['login'], $pprow['password'])) {
675 return xl('Login to remote host') . " '$remote_host' " . xl('failed');
677 $files = $sftp->nlist($pathname);
678 foreach ($files as $file) {
679 if (substr($file, 0, 1) == '.') continue;
680 ++$filecount;
681 if (!isset($info["$ppid/$file"])) $info["$ppid/$file"] = array();
682 // Ensure that archive directory exists.
683 $prpath = $GLOBALS['OE_SITE_DIR'] . "/procedure_results";
684 if (!file_exists($prpath)) mkdir($prpath);
685 $prpath .= '/' . $pprow['ppid'];
686 if (!file_exists($prpath)) mkdir($prpath);
687 // Get file contents.
688 $hl7 = $sftp->get("$pathname/$file");
689 // If user requested reject and delete, do that.
690 if (!empty($info["$ppid/$file"]['delete'])) {
691 $fh = fopen("$prpath/$file.rejected", 'w');
692 if ($fh) {
693 fwrite($fh, $hl7);
694 fclose($fh);
696 else {
697 return xl('Cannot create file') . ' "' . "$prpath/$file.rejected" . '"';
699 if (!$sftp->delete("$pathname/$file")) {
700 return xl('Cannot delete (from SFTP server) file') . ' "' . "$pathname/$file" . '"';
702 continue;
704 // Do a dry run of its contents and check for errors and match requests.
705 $tmp = receive_hl7_results($hl7, $ppid, $pprow['direction'], true, $info["$ppid/$file"]['select']);
706 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
707 $info["$ppid/$file"]['match'] = $tmp['match'];
708 if (!empty($tmp['fatal']) || !empty($tmp['match'])) {
709 // There are errors or matching requests so skip this file.
710 continue;
712 // Now the money shot - not a dry run.
713 $tmp = receive_hl7_results($hl7, $ppid, $pprow['direction'], false, $info["$ppid/$file"]['select']);
714 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
715 $info["$ppid/$file"]['match'] = $tmp['match'];
716 if (empty($tmp['fatal']) && empty($tmp['match'])) {
717 // It worked, archive and delete the file.
718 $fh = fopen("$prpath/$file", 'w');
719 if ($fh) {
720 fwrite($fh, $hl7);
721 fclose($fh);
723 else {
724 return xl('Cannot create file') . ' "' . "$prpath/$file" . '"';
726 if (!$sftp->delete("$pathname/$file")) {
727 return xl('Cannot delete (from SFTP server) file') . ' "' . "$pathname/$file" . '"';
730 } // end of this file
731 } // end SFTP
733 else if ($protocol == 'FS') {
734 // Filesystem directory containing results files.
735 $pathname = $pprow['results_path'];
736 if (!($dh = opendir($pathname))) {
737 return xl('Unable to access directory') . " '$pathname'";
739 // Sort by filename just because.
740 $files = array();
741 while (false !== ($file = readdir($dh))) {
742 if (substr($file, 0, 1) == '.') continue;
743 $files[$file] = $file;
745 closedir($dh);
746 ksort($files);
747 // For each file...
748 foreach ($files as $file) {
749 ++$filecount;
750 if (!isset($info["$ppid/$file"])) $info["$ppid/$file"] = array();
751 // Ensure that archive directory exists.
752 $prpath = $GLOBALS['OE_SITE_DIR'] . "/procedure_results";
753 if (!file_exists($prpath)) mkdir($prpath);
754 $prpath .= '/' . $pprow['ppid'];
755 if (!file_exists($prpath)) mkdir($prpath);
756 // Get file contents.
757 $hl7 = file_get_contents("$pathname/$file");
758 // If user requested reject and delete, do that.
759 if (!empty($info["$ppid/$file"]['delete'])) {
760 $fh = fopen("$prpath/$file.rejected", 'w');
761 if ($fh) {
762 fwrite($fh, $hl7);
763 fclose($fh);
765 else {
766 return xl('Cannot create file') . ' "' . "$prpath/$file.rejected" . '"';
768 if (!unlink("$pathname/$file")) {
769 return xl('Cannot delete file') . ' "' . "$pathname/$file" . '"';
771 continue;
773 // Do a dry run of its contents and check for errors and match requests.
774 $tmp = receive_hl7_results($hl7, $ppid, $pprow['direction'], true, $info["$ppid/$file"]['select']);
775 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
776 $info["$ppid/$file"]['match'] = $tmp['match'];
777 if (!empty($tmp['fatal']) || !empty($tmp['match'])) {
778 // There are errors or matching requests so skip this file.
779 continue;
781 // Now the money shot - not a dry run.
782 $tmp = receive_hl7_results($hl7, $ppid, $pprow['direction'], false, $info["$ppid/$file"]['select']);
783 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
784 $info["$ppid/$file"]['match'] = $tmp['match'];
785 if (empty($tmp['fatal']) && empty($tmp['match'])) {
786 // It worked, archive and delete the file.
787 $fh = fopen("$prpath/$file", 'w');
788 if ($fh) {
789 fwrite($fh, $hl7);
790 fclose($fh);
792 else {
793 return xl('Cannot create file') . ' "' . "$prpath/$file" . '"';
795 if (!unlink("$pathname/$file")) {
796 return xl('Cannot delete file') . ' "' . "$pathname/$file" . '"';
799 } // end of this file
800 } // end FS protocol
802 // TBD: Insert "else if ($protocol == '???') {...}" to support other protocols.
804 } // end procedure provider
806 // echo "<!-- out: "; print_r($info); echo " -->\n"; // debugging
808 return '';
810 // PHP end tag omitted intentionally.