RSS Git Download  Clone
Raw Blame History
package LIMS::Local::HL7;

use Moo; # using faster Moo
use LIMS::Local::MooX::Types qw(NetHL7Request HashReference);

has msg => (
	is 		=> 'ro',
	isa 	=> NetHL7Request, # MooX::Types::MooseLike def
#	isa 	=> sub { ref $_[0] eq 'Net::HL7::Request' }, non-MooX::Types::MooseLike
	default => sub { new Net::HL7::Request() }
);
has settings  => ( is => 'ro', required => 1, isa => HashReference ); 
#__PACKAGE__->meta->make_immutable; # Moo does this automatically

use Net::HL7::Request;
use Net::HL7::Message;
use Net::HL7::Segment;
use Net::HL7::Connection;
use Net::HL7::Segments::MSH;

use Text::Wrap qw($columns fill); $columns = 68;
use LIMS::Local::Utils;
use Data::Dumper;

use Regexp::Common qw(pattern);
# ==============================================================================
use constant SLASH => "\\"; # have to escape slash !! 
{
	pattern name => [ qw(hl7 escape_character) ],
		create => quotemeta "\\", # not used in ->subs() context
		subs   => sub {
			my $a = quotemeta "\\";
			my $b = SLASH.'E'.SLASH; # \ => \E\
			$_[1] =~ s/$a/$b/g;
		};
	pattern name => [ qw(hl7 field_delimiter) ],
		create => quotemeta '|', # not used in ->subs() context
		subs   => sub {
			my $a = quotemeta '|';
			my $b = SLASH.'F'.SLASH; # | => \F\
			$_[1] =~ s/$a/$b/g;
		};
	pattern name => [ qw(hl7 repetition_separator) ],
		create => quotemeta '~', # not used in ->subs() context
		subs   => sub {
			my $a = quotemeta '~';
			my $b = SLASH.'R'.SLASH; # ~ => \R\
			$_[1] =~ s/$a/$b/g;
		};
	pattern name => [ qw(hl7 component_delimiter) ],
		create => quotemeta '^', # not used in ->subs() context
		subs   => sub {
			my $a = quotemeta '^';
			my $b = SLASH.'S'.SLASH; # ^ => \S\
			$_[1] =~ s/$a/$b/g;
		};
	pattern name => [ qw(hl7 sub_component_delimiter) ],
		create => quotemeta '&', # not used in ->subs() context
		subs   => sub {
			my $a = quotemeta '&';
			my $b = SLASH.'T'.SLASH; # & => \T\
			$_[1] =~ s/$a/$b/g;
		};
}
# ==============================================================================

sub _build_msg { new Net::HL7::Request() }

my $now = LIMS::Local::Utils::time_now();
my $wrap_text = sub { LIMS::Local::Utils::text_wrap(@_) };

# ------------------------------------------------------------------------------
sub build_msh_segment {
	my $self = shift;
	
	my $seg = new Net::HL7::Segments::MSH();
	
	# MSH field 3 Sending application
	$seg->setField(3, 'HILIS');
	# MSH field 4 Sending facility
	$seg->setField(4, 'HMDS');
	# MSH field 5 Receiving application
	$seg->setField(5, 'Ensemble');
	# MSH field 6 Receiving facility
	$seg->setField(6, 'Ensemble');
	# MSH field 7 date and time of message in format YYYYMMDD[HHMM]
	$seg->setField(7, $now->strftime('%Y%m%d%H%M') );
	# MSH field 9 message type
	$seg->setField(9, 'ORU^R01');
	# MSH field 10 message control ID
	$seg->setField(10, sprintf 'hmds_%s.001', $now->strftime('%Y%m%d'));
	# MSH field 11 processing ID, P for production
	$seg->setField(11, 'P');
	# MSH field 12 version ID 2.4
	$seg->setField(12, '2.4');
	
	$self->msg->addSegment($seg); # warn Dumper $self->msg;
}

# ------------------------------------------------------------------------------ 
sub build_pid_segment {
	my ($self, $vars) = @_;

	my $seg = new Net::HL7::Segment('PID');

	my $NULL = ( $self->settings->{hl7d_addr} eq '127.0.0.1' )
        ? $Net::HL7::NULL # to prevent uninitialized warnings in HL7d
        : undef; # warn $NULL;

	my $first_name	= $vars->{first_name};
	my $middle_name	= $vars->{middle_name} || $NULL;
	my $last_name	= $vars->{last_name};
	my $unit_number = $vars->{unit_number};
	my $nhs_number  = $vars->{nhs_number};
	my $gender		= $vars->{gender} || 'U';
	my $dob			= $vars->{dob};
	
	# eliminate dob separators:
	$dob =~ s/\D//g; # warn $dob;

	# PID field 1 PID ID, will only ever be 1 as only 1 patient per message
	$seg->setField(1, 1);	
	{ # PID field 3, local hospital number followed by NHS number (if exists):
		my $str = $nhs_number
			? "$unit_number^$NULL^$NULL^MRN~$nhs_number^$NULL^$NULL^NHS"
			: "$unit_number^$NULL^$NULL^MRN";
		$seg->setField(3, $str);
	}
	# PID field 5 surname and forename
	$seg->setField(5, join '^', $last_name, $first_name, $middle name );
	# PID field 7 DOB format is YYYYMMDD
	$seg->setField(7, $dob);
	# PID field 8 gender, F � Female, M � Male, U � Unknown
	$seg->setField(8, $gender);
	
	$self->msg->addSegment($seg); # warn Dumper $self->msg;
}

# ------------------------------------------------------------------------------
sub build_pv1_segment { # patient visit 1
	my ($self, $vars) = @_;

	my $seg = new Net::HL7::Segment('PV1');
	
	my $organisation_code = $vars->{organisation_code};
	my $referrer_code 	  = $vars->{referrer_code};
	my $referrer_name  	  = $vars->{referrer_name};
	
	my $location = _hl7_escape($vars->{location});
	
	# PV1 field 3 location national code^description^national code
	$seg->setField(3, "$organisation_code^$location^$organisation_code");
	# PV1 field 8 Referring Doctor National Code^Name
	$seg->setField(8, "$referrer_code^$referrer_name");
	# PV1 field 9 Consulting Doctor National Code^Name
#	$seg->setField(9, "$referrer_code^$referrer_name"); # do we need this ??
	
	$self->msg->addSegment($seg); # warn Dumper $self->msg;
}

# ------------------------------------------------------------------------------
# ORC segment - Order Control with sample number, requesting clin, etc
sub build_orc_segment {
	my ($self, $vars) = @_;

	my $seg = new Net::HL7::Segment('ORC');
	
	my $organisation_code	= $vars->{organisation_code};
	my $referrer_code 		= $vars->{referrer_code};
	my $referrer_name		= $vars->{referrer_name};
	my $request_number 		= _get_request_number($vars);
	
	# ORC field 1 message type, RE - Results Transaction
	$seg->setField(1, 'RE');
	# ORC field 3 sample number
	$seg->setField(3, $request_number);
	# ORC field 12 requesting clinician - local code^name
	$seg->setField(12, "$referrer_code^$referrer_name^$referrer_code");
	# ORC field 13 requesting location code
	$seg->setField(13, $organisation_code);
	
	$self->msg->addSegment($seg); # warn Dumper $self->msg;
}

# ------------------------------------------------------------------------------
# OBR segment - Observation Request Segment acting as report header with request
# date, time, etc
sub build_obr_segment {
	my ($self, $vars) = @_;

	my $seg = new Net::HL7::Segment('OBR');
	
	my $organisation_code	= $vars->{organisation_code};
	my $referrer_code 		= $vars->{referrer_code};
	my $request_date		= $vars->{created_at};
	my $auth_date			= $vars->{updated_at};
	my $referrer			= $vars->{referrer_name};
	my $specimen 			= $vars->{specimen};
	
	my $request_number 		= _get_request_number($vars);
	
	# convert dates to ICE format	
	$request_date =~ s/\D//g;
	$auth_date    =~ s/\D//g;

	# OBR field 1 Set ID, incrementing number per segment
	$seg->setField(1, 1);
	# OBR field 3 Filler Order Number (use sample number)
	$seg->setField(3, $request_number);
	# OBR field 4 Universal Service Identifier (identifierST^text^nameofcodingsystem)
	$seg->setField(4, 'DSR^HMDS Report^LC');
	# OBR field 6 collection date/time in format ccyyMMddHHmm, does not exist
	# OBR field 7 Observation date/time	
	$seg->setField(7, $request_date);
	# OBR field 14 Recieve date/time in format ccyyMMddHHmm (time sample requested)
	$seg->setField(14, $request_date);
	{ # OBR Specimen Source
		my @specimen = split ',', $specimen;
		$seg->setField(15, join '&', @specimen;
	}
	{ # OBR field 16 Ordering Provider
		my ($lname, $inits) = split ' ', $referrer;
		$seg->setField(16, "$lname^$NULL^$inits");
	}
	# OBR field 22 report date/time in format ccyyMMddHHmm
	$seg->setField(22, $auth_date);
	# OBR field 24 speciality code (General Pathology)
	$seg->setField(24, '820');
	# OBR field 25 status : final report
 	$seg->setField(25, 'F');
	# OBR field 32 Principal Result Interpreter
	
	
	$self->msg->addSegment($seg); # warn Dumper $self->msg;
}

# ------------------------------------------------------------------------------
# OBX segment - Observation Result Segment
sub build_obx_segment {
	my ($self, $vars) = @_;

	my $seg = new Net::HL7::Segment('OBX');
	
	my $sample_number = _get_request_number($vars);
	my $request_date  = $vars->{created_at};
  
	$seg->setField(1, 1);
	# OBX field 2 data type - ST standard text
	$seg->setField(2, 'ST');
	# OBX field 3 investigation code ()
	$seg->setField(3, 'DSR^HMDS Report^LC');  
	
	# OBX field 4 UniversalServiceIdentifier (required)
	$seg->setField(4, 1);
	# OBX field 14 collection date (using date booked in)
	$seg->setField(14, $request_date);
	# OBX field 15 sample number
	$seg->setField(15, $sample_number);
	
	$self->msg->addSegment($seg); # warn Dumper $self->msg;
}

# ------------------------------------------------------------------------------
sub build_nte_segment { # notes and comments
	my ($self, $vars) = @_;

	my $seg = new Net::HL7::Segment('NTE');
	
	my $specimen = $vars->{specimen};
	my $quality	 = $vars->{specimen_quality} || '';
	
	# NTE field 1 Set ID, Sequential Number incrementing by 1 for each NTE
	$seg->setField(1, 1);
	# NTE field 2 Source of Comment C = Clinical Comment follows PID
	# R = Result Comment Usually follows an OBR or OBX
	$seg->setField(2, 'R');
	# NTE field 3 free text comment, in this case specimen details
	$seg->setField(3, "Specimen Data: $specimen, Quality: $quality");
	
	$self->msg->addSegment($seg); # warn Dumper $self->msg;
}

# ------------------------------------------------------------------------------	
sub build_report_segment {
	my ($self, $vars) = @_; # warn Dumper $vars;

	# comment:
	my $comment = _hl7_escape($vars->{summaries}); # warn $comment;

	# wrap long lines:
	my $text = $self->_wrap_text($comment, 68); # warn $text;
	
	my $i = 0; # field 1 counter
	# split $text into rows, add each as a new segment with '|' appended:
	for my $line ( split "\n", $text ) { # warn $line;
		$line .= '|'; # effectively replace "\n" with '|'
		my $seg = new Net::HL7::Segment('NTE');
		$seg->setField(1, ++$i); # autoincrement before use
		$seg->setField(2, 'I');
		$seg->setField(3, $line); 
		$self->msg->addSegment($seg);
	}	
}

# ------------------------------------------------------------------------------
sub send_message {
	my ($self, $vars) = @_; # warn Dumper $vars;

	my $msg = $self->msg; # warn Dumper $msg; # Net::HL7::Request object

    my $cfg = $self->settings; # warn Dumper $cfg;
    
	# open HL7 connection via socket
	my $hl7d = new Net::HL7::Connection($cfg->{hl7d_addr}, $cfg->{hl7d_port})
	or die 'Could not connect to HL7d on port ' . $cfg->{hl7d_port};

	# send the HL7 message and wait for acknowledgement
	my $response = $hl7d->send($msg);
    
	# set default error - what ??
	my $error = "Acknowledgement received";
	
	# process hl7 response and correct error accordingly
	if ($response) {
        my $msa_seg = $response->getSegmentByIndex(1);
        # retrieve field 1 of MSA segment
        my $msa_field1 = $msa_seg->getFields(1, 1); # warn $msa_field1;

        # initially set description as successful message
        my $description = "successful transaction";
	
        # check there is no error
        if ( $msa_field1 ne "AA" ) {
            # retrieve field 3 of MSA segment (description of error).
            $description = $msa_seg->getFields(1, 3); # warn $description;
        }
    }
    
	$hl7d->close();
    
    return $error;
	# print $msg->toString(1)."\n";
	# wait five seconds before processing next message
	# sleep 5; # why? - does ICE require this?
}

# --------------------------------------------------------
sub _hl7_escape {
	my $str = shift;
	
=begin # line-noise:
	$str =~ s/\\/\\E\\/g;
	$str =~ s/&/\\T\\/g;
	$str =~ s/\|/\\F\\/g;
	$str =~ s/\^/\\S\\/g;
	$str =~ s/~/\\R\\/g;
	return $str;
=cut	

	$str = $RE{hl7}{escape_character}->subs($str); # DO THIS FIRST
	$str = $RE{hl7}{field_delimiter}->subs($str);
	$str = $RE{hl7}{component_delimiter}->subs($str);
	$str = $RE{hl7}{repetition_separator}->subs($str);
	$str = $RE{hl7}{sub_component_delimiter}->subs($str);
	return $str;
}

# format text: indent, max 68 chars / line:
sub _wrap_text { # Text::Wrap
	my ($self, $str, $cols) = @_;
	
	my $indent = $self->settings->{indent};
	my @args = ( $indent, $indent, $str );
	my $formatted = LIMS::Local::Utils::text_wrap($cols, \@args); # warn $formatted;
	return $formatted; 
}

sub _get_request_number {
	my $vars = shift;
	return join '/', $vars->{request_number}, $vars->{year} - 2000;
}

1;