#!/usr/bin/env perl =begin usage: perl $0 [-tlqr] -d [-p ] * --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