RSS Git Download  Clone
Raw Blame History
#!/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
* --reqId 10 --reqId 100 --reqId 10000, etc

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
my @REQUEST_IDS     = ();   # --request_id 1 --request_id 2, etc

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
    "reqId=i"     => \@REQUEST_IDS,     # int list (optional)
); # 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 = (
    'RA301', # Weston General
    '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);
_open_logfiles(); # only executed if called with -l or -t command opts

# 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;
}
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->{speciality_code}      = 820; # General Pathology
    # $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 _open_logfiles { # if called as command using --localhost|l or --test|t opts:
    return unless $LOCALHOST || $TESTING;
    open $hl7_log, '>', $app_dir . '/logs/hl7_log.txt' or die $!;
    open $hl7_ice, '>', $app_dir . '/logs/hl7_ice.txt' or die $!;
}

# ------------------------------------------------------------------------------
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;
        printf $hl7_log "%s\n", $msg->toString(1);
        # print  $hl7_log Dumper $msg;
        printf $hl7_log "%s\n", "=" 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
    # require 'DISTINCT' on section_name to prevent duplication of section result:
    my $rs = q!
        GROUP_CONCAT(
            DISTINCT(ls.section_name), ':', rrs.results_summary SEPARATOR "\n\n"
        )!; # 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 AS 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 },
                },
            ],
        ]
    );
    # override %where if @REQUEST_IDS exists:
    %where = ( 'r.id' => { -in => \@REQUEST_IDS } ) if @REQUEST_IDS;

    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