#!/usr/bin/env perl
=begin
usage: perl $0 [-tlqr] -d <days> [-p <port>]
* --localhost|l = use localhost HL7d (requires hl7d running)
* --query|q = output query to stdout
* --test|t = use remote test server IP::port
* --response|r = output remote HL7 server response
to use hl7d:
* config in /usr/local/hl7/hl7d.conf
* plugins /usr/local/hl7/plugins
* start hl7d: cd /usr/local/hl7d && sudo perl -T hl7d.pl --nodetach --debug
use $Net::HL7::NULL (double-quotation marks), or default (undef) for null fields
* use of default (undef) generates following warning: "Use of uninitialized value
$field in concatenation (.) or string at Net/HL7/Segment.pm line 187"
reason: my $field = $self->{FIELDS}->[$index];
$self->{FIELDS} = [
[0] "OBX",
...
[9] undef,
[10] undef,
[11] "F"
so index value of 9 or 10 will trigger warning - suppressed using Net::HL7::NULL
[9] ""
[10] ""
$Net::HL7::NULL set to 'true' if running under test mode (hl7d complains about undef fields)
=cut
# $ENV{CENTRE} = 'bristol'; # not required - setting sysadmin_email manually
# set default command-line opts:
my $RESPONSE_OUTPUT = 0; # --response|r - output HL7 server response
my $QUERY_OUTPUT = 0; # --query|q - output sql queries to console
my $MYSQL_PORT = 3306; # --port|p - eg 3307 via localhost tunnel for testing
my $LOCALHOST = 0; # --localhost|l - use localhost hl7d
my $DURATION = 1; # --days|d - duration in days
my $TESTING = 0; # --test|t - use remote test server IP::port
use Getopt::Long;
GetOptions (
"localhost|l" => \$LOCALHOST, # flag
"response|r" => \$RESPONSE_OUTPUT, # flag
"days|d=i" => \$DURATION, # int
"port|p=i" => \$MYSQL_PORT, # int
"query|q" => \$QUERY_OUTPUT, # flag
"test|t" => \$TESTING, # flag
); # warn $DURATION; warn $LOCALHOST; exit;
die "cannot select both --localhost|l and --test|t (remote address) options\n"
if $LOCALHOST && $TESTING;
use strict;
use warnings;
use lib '/home/raj/perl5/lib/perl5';
use Data::Printer alias => 'p';
use Net::HL7::Connection;
use SQL::Abstract::More;
use FindBin qw($Bin); # warn $Bin;
use Data::Dumper;
use lib '/home/raj/perl-lib';
use Local::DB;
use lib $Bin . '/../../../lib';
use LIMS::Local::ScriptHelpers;
use LIMS::Local::Config; # to override sysadmin
use LIMS::Local::Utils;
use LIMS::Local::HL7;
# ==============================================================================
$ENV{SQL_TRACE} = $QUERY_OUTPUT; # warn $ENV{SQL_TRACE}; switch on query trace
# does ICE server expect empty strings or undef fields?
my $use_hl7_null = 0; # $Net::HL7::NULL (double-quotation marks), or undef
my @locations = (
'RA701', # BRI
'RA709', # Bristol Dental Hospital
'RA710', # Bristol Haematology & Oncology Ctr
'RA723', # Bristol Childrens Hospital
'RA799', # Bristol Oncology Centre (ficticious code)
'RD130', # RUH, Bath
'RVJ01', # Southmead
# 'RVJ20', # Frenchay (defunct)
); # also including all GP practices from Aug/2018
# ==============================================================================
my $app_dir = $Bin . '/../../../'; # warn $app_dir;
my $tools = LIMS::Local::ScriptHelpers->new();
my $now = LIMS::Local::Utils::time_now();
my $cfg_file = $app_dir . '/script/crons/lib/settings.cfg';
my $settings = Config::Auto::parse($cfg_file); # p $settings;
my $ref_date = $now->clone->subtract(days => $DURATION)->ymd;
# set null character - $Net::HL7::NULL (double-quotation marks), or undef:
$settings->{use_hl7_null} = $use_hl7_null;
{ # override global sysadmin_email for this script - persists due to instance():
my $app_config = LIMS::Local::Config->instance; # p $app_config;
$app_config->{settings}->{sysadmin_email}
= $settings->{nbt_ice_server}->{admin_email};
}
my ($hl7_log, $hl7_ice);
# set HL7 server addr & port:
if ($TESTING) { # remote test server:
$settings->{hl7d_addr} = $settings->{nbt_ice_server}->{_test_addr};
$settings->{hl7d_port} = $settings->{nbt_ice_server}->{_test_port};
}
elsif ($LOCALHOST) { # localhost hl7d:
$settings->{hl7d_addr} = 'localhost';
$settings->{hl7d_port} = 12002;
$settings->{use_hl7_null} ||= 1;
# don't open logs unless called as command using --localhost|l opt:
open $hl7_log, '>', $app_dir . '/logs/hl7_log.txt' or die $!;
open $hl7_ice, '>', $app_dir . '/logs/hl7_ice.txt' or die $!;
}
else { # remote live server:
$settings->{hl7d_addr} = $settings->{nbt_ice_server}->{remote_addr};
$settings->{hl7d_port} = $settings->{nbt_ice_server}->{remote_port};
} # p $settings; exit;
my $dbix = do {
my %h = (
dbname => 'bristol',
port => $MYSQL_PORT,
);
Local::DB->dbix(\%h);
};
# increase max length of group_concat; permitted range (32-bit system) 4 .. 4294967295
$dbix->dbh->do('SET group_concat_max_len = 3072');
my $nbt_user_id
= $dbix->select('users', 'id', { username => 'nbt-bhods' })->value;
# get sql statments:
my ($sql, @bind) = _get_query_params();
my @requests = $dbix->query($sql, @bind)->hashes; # p @requests; exit;
exit unless @requests;
# args for LIMS::Local::HL7->new():
my $hl7_socket = _build_hl7_socket(); # open connection to hl7 server once only
my %hl7_args = (
sending_application => 'ULTRA',
receiving_facility => 'ICE00',
sending_facility => 'BHODS',
receiving_system => 'ICE',
hl7_socket => $hl7_socket,
settings => $settings,
); # p %hl7_args; exit;
my $i = 0; # auto-incrementing message id
for my $vars ( @requests ) { # p $vars; next;
my $hl7 = LIMS::Local::HL7->new(\%hl7_args); # pass socket & settings args
# custom fields:
$vars->{observation_datetime} = $vars->{created_at}; # OBR#7
# $vars->{requested_datetime} = ; # OBR#6 (using OBR#7 for this)
$vars->{order_control} = 'RE'; # RE = Results Transaction
$vars->{message_type} = 'ORU^R01';
$vars->{lab_number} = $hl7->get_lab_number($vars); # ORC#3
# send data to hl7 object:
$hl7->dataset($vars);
$hl7->msg_count(++$i);
# build HL7 message:
$hl7->build_msh_segment;
$hl7->build_pid_segment;
$hl7->build_nte_segment;
$hl7->build_pv1_segment;
$hl7->build_orc_segment;
$hl7->build_obr_segment;
# $hl7->build_obx_segment($vars); # handled by _build_report()
# $hl7->build_report_segment($vars); # handled by _build_report()
# add report (comment, diagnosis, etc) to HL7 message:
_build_report($hl7, $vars);
# message should now be complete, send it
my $response = $hl7->send_message(); # p $response;
# dump msg, vars & response to log (and stdout if test mode):
_dump_msg($vars, $hl7->msg, $response);
}
$hl7_socket->close();
# ------------------------------------------------------------------------------
sub _build_report {
my ($hl7, $vars) = @_;
# create text string from samples code(s):
$vars->{specimen_type} = $hl7->specimen_summary($vars->{sample_code});
my @report = (
[ 'Specimen type', $vars->{specimen_type} ],
[ 'Specimen quality', $vars->{specimen_quality} ],
[ 'Gross description', $vars->{gross_description} ],
[ 'Results summary', $vars->{result_summaries} ],
[ 'Clinical details', $vars->{clinical_details} ],
[ 'Morphology/comment', $vars->{comment} ],
[ 'Diagnosis', $vars->{diagnosis} ],
[ 'ICDO3', $vars->{icdo3} ],
);
my $n = 0; # incremental OBX counter
$hl7->build_obx_segment(@$_, ++$n) for @report; # field, value, counter
}
# ------------------------------------------------------------------------------
sub _build_hl7_socket {
# open HL7 connection via socket
my ($addr, $port) = @{$settings}{ qw/hl7d_addr hl7d_port/ };
if ( my $socket = new Net::HL7::Connection($addr, $port) ) {
return $socket;
} # no socket:
my $msg = "Could not connect to HL7d $addr:$port - $!";
$tools->mail_admin({ script => 'NBT ICE HL7', msg => $msg })
unless $addr eq 'localhost'; # NBT sysadmin doesn't need to know!
die $msg;
}
# ------------------------------------------------------------------------------
sub _dump_msg { # write to interface logs
my ($vars, $msg, $response) = @_; # warn Dumper $msg;
# $vars - hashref of request data
# $msg - HL7 msg object
# $response - hl7d return string
# log to db or output to stdout & log files:
if ( $LOCALHOST || $TESTING ) {
p $msg->toString(1); # Net::HL7::Request object
p $response;
printf $hl7_log "%s: %s\n\n", $vars->{lab_number}, $response;
print $hl7_log $msg->toString(1)."\n";
# print $hl7_log Dumper $msg;
print $hl7_log "=" x 100;
printf $hl7_ice "%s %s\n", $vars->{lab_number}, '=' x 100;
while ( my @pair = map { $_ ||= 'NULL' } each %$vars ) {
printf $hl7_ice "%s: %s\n", @pair;
}
}
else { # log to db:
my $request_id = $vars->{request_id};
{ # request_hl7_log:
my %h = (
request_id => $request_id,
response => $response,
service => 'ICE',
); # p %h;
$dbix->insert('request_hl7_log', \%h);
}
{ # request_history:
my %h = (
request_id => $request_id,
user_id => $nbt_user_id,
action => 'dispatched request data to ICE',
); # p %h;
$dbix->insert('request_history', \%h);
}
}
print $response if $RESPONSE_OUTPUT; # flag passed by HILIS, returns HL7 response
}
# ------------------------------------------------------------------------------
sub _get_request_number {
my $vars = shift;
return join '/', $vars->{request_number}, $vars->{year} - 2000;
}
sub _get_query_params { # bristol don't authorise so using report_date
my $rs = q!
group_concat(distinct(ls.section_name),':',rrs.results_summary,'|')!;
my @cols = (
qw/
p.last_name
p.first_name
p.middle_name
p.dob
p.nhs_number
p.gender
pc.unit_number
rs.display_name|location
rs.organisation_code
ref.name|referrer_name
ref.national_code|referrer_code
r.id|request_id
r.request_number
r.year
r.created_at
r.updated_at
rr.status
rr.specimen_quality
group_concat(distinct(s.description))|specimen
group_concat(distinct(s.sample_code))|sample_code
d.name|diagnosis
d.icdo3
rsv.username|reporter
rsv.time|report_datetime
/,
"$rs|result_summaries",
); # p @cols;
my @rels = (
'requests|r' => q{r.patient_case_id=pc.id} ,
'patient_case|pc' => q{pc.patient_id=p.id} ,
'patients|p' => q{pc.referral_source_id=rs.id} ,
'referral_sources|rs' => q{r.referrer_department_id=rd.id} ,
'referrer_department|rd' => q{rd.referrer_id=ref.id} ,
'referrers|ref' => q{rh.request_id=r.id} ,
'request_history|rh' => q{rr.request_id=r.id} ,
'request_report_view|rr' => q{rr.diagnosis_id=d.id} ,
'diagnoses|d' => q{rrs.request_id=r.id} ,
'request_result_summaries|rrs' => q{rrs.lab_section_id=ls.id} ,
'lab_sections|ls' => q{rs2.request_id=r.id} ,
'request_specimen|rs2' => q{rs2.specimen_id=s.id} ,
'specimens|s' => q{=>rdh.request_id=r.id} ,
'request_diagnosis_history|rdh' =>
q{rsv.request_id=r.id,rsv.action='reported'},
'request_status_view|rsv' => q{rs.referral_type_id=rt.id} ,
'referral_types|rt'
);
# reported, request_report updated, request_diagnosis_history updated, or
# patient table updated:
my %where = (
-and => [
[
'rs.organisation_code' => { -in => \@locations },
'rt.description' => 'practice',
],
'rh.action' => 'reported',
-or => [
'DATE(rh.time)' => $ref_date,
'DATE(rdh.time)' => $ref_date,
'DATE(rr.updated_at)' => $ref_date,
-and => {
'DATE(p.updated_at)' => $ref_date,
'DATE(rh.time)' => { '<' => $ref_date },
},
],
]
);
my @args = (
-columns => \@cols,
-from => [ -join => @rels ],
-where => \%where,
-group_by => 'r.id',
-order_by => 'r.id',
); # p @args;
my ($sql, @bind) = SQL::Abstract::More->new->select(@args); # p $sql; p \@bind;
# $dbix->dump_query($sql, @bind); # exit;
return ($sql, @bind);
}
=BEGIN
# ------------------------------------------------------------------------------
sub _get_query_params {
my $sql = _get_sql(); # library.sql [ice_report] entry
# count number of placeholders in $sql:
my $n = () = $sql =~ /\?/g; # p $n;
# bind one $duration per placeholder:
my @bind = map $duration, (1..$n); # p \@bind;
return ($sql, @bind);
}
# ------------------------------------------------------------------------------
sub _get_sql {
return q!
SELECT
p.last_name,
p.first_name,
p.middle_name,
p.dob,
p.nhs_number,
p.gender,
pc.unit_number,
rs.display_name as 'location',
rs.organisation_code,
rf.name as 'referrer_name',
rf.national_code as 'referrer_code',
req.id as 'request_id',
req.request_number,
req.year,
req.created_at,
req.updated_at,
rr.status,
rr.specimen_quality,
group_concat(distinct sp.description) as 'specimen',
group_concat(distinct sp.sample_code) as 'sample_code',
d.name as 'diagnosis',
d.icdo3,
rsv.username as 'report_by',
rsv.time as 'auth_date', /* bristol don't authorise */
group_concat( distinct ls.section_name,':',rrs.results_summary,'|')
as 'result_summaries'
FROM
requests req
join patient_case pc on req.patient_case_id = pc.id
join patients p on pc.patient_id = p.id
join referral_sources rs on pc.referral_source_id = rs.id
join referrer_department rd on req.referrer_department_id = rd.id
join referrers rf on rd.referrer_id = rf.id
join request_history h on h.request_id = req.id
join request_report_view rr on rr.request_id = req.id
join diagnoses d on rr.diagnosis_id = d.id
join request_result_summaries rrs on rrs.`request_id` = req.id
join lab_sections ls on rrs.`lab_section_id` = ls.id
join request_specimen rsp on rsp.`request_id` = req.id
join specimens sp on rsp.`specimen_id` = sp.id
join request_status_view rsv on rsv.request_id = req.id
and rsv.`action` = 'reported'
left join `request_diagnosis_history` rdh on rdh.`request_id` = req.id
WHERE
( /* reported, request_report updated, request_diagnosis_history updated, or patient table updated: */
( h.`action` = 'reported' and
DATE(h.`time`) = DATE_SUB(CURRENT_DATE(), INTERVAL ? DAY) ) or
DATE(rr.`updated_at`) = DATE_SUB(CURRENT_DATE(), INTERVAL ? DAY) or
DATE(rdh.`time`) = DATE_SUB(CURRENT_DATE(), INTERVAL ? DAY) or
( /* previously reported: */
DATE(h.`time`) < DATE_SUB(CURRENT_DATE(), INTERVAL ? DAY) and
DATE(p.`updated_at`) = DATE_SUB(CURRENT_DATE(), INTERVAL ? DAY)
)
AND rs.organisation_code IN (??)
)
GROUP BY req.id!;
}
=cut