package LIMS::Local::HL7; use Moo; # default seems to be to die on uninitialized warnings use LIMS::Local::MooX::Types qw(NetHL7Request NetHL7Connection HashReference String); has msg => ( is => 'ro', # 'lazy' is valid Moo option also (does same as lazy_build) isa => NetHL7Request, # MooX::Types::MooseLike def # isa => sub { ref $_[0] eq 'Net::HL7::Request' }, non-MooX::Types::MooseLike default => sub { new Net::HL7::Request() }, # returns on 1st call ); has hl7_socket => ( is => 'ro', isa => NetHL7Connection, required => 1 ); has msg_count => ( is => 'rw', isa => String ); has settings => ( is => 'ro', isa => HashReference, required => 1 ); has dataset => ( is => 'rw', isa => HashReference, coerce => \&_stringify_datetimes ); has $_ => ( is => 'ro', isa => String, required => 1 ) for qw( sending_application receiving_facility receiving_system sending_facility ); use Net::HL7::Request; use Net::HL7::Message; use Net::HL7::Segment; use Net::HL7::Segments::MSH; use LIMS::Local::Utils; use Data::Printer alias => 'p'; 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; }; } # ============================================================================== my $now = LIMS::Local::Utils::time_now(); my $wrap_text = sub { LIMS::Local::Utils::text_wrap(@_) }; # ------------------------------------------------------------------------------ sub build_msh_segment { my $self = shift; my $vars = $self->dataset; my $seg = new Net::HL7::Segments::MSH(); # MSH field 3 Sending application $seg->setField(3, $self->sending_application); # MSH field 4 Sending facility $seg->setField(4, $self->sending_facility); # MSH field 5 Receiving application $seg->setField(5, $self->receiving_system); # MSH field 6 Receiving facility $seg->setField(6, $self->receiving_facility); # 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, $vars->{message_type}); { # MSH field 10 message control ID my $id = sprintf 'hmds_%s.%03d', $now->strftime('%Y%m%d'), $self->msg_count; $seg->setField(10, $id); } # MSH field 11 processing ID, P for production $seg->setField(11, 'P'); # MSH field 12 version ID 2.4 $seg->setField(12, '2.4'); my $field_count = 12; # for fill_undefs() if ( my $val = $vars->{accept_acknowledgement_type} ) { $seg->setField(15, $val); $field_count += 2; # will have blanks fields 13 & 14 before #15 } if ( my $val = $vars->{application_acknowledgement_type} ) { $seg->setField(16, $val); } # avoid uninitialized warnings in HL7::Segment using hl7d: $self->fill_undefs($seg, 1 .. $field_count); # p $seg; $self->msg->addSegment($seg); } # ------------------------------------------------------------------------------ sub build_pid_segment { my $self = shift; my $NULL = $self->_get_null_field(); my $vars = $self->dataset; my $seg = new Net::HL7::Segment('PID'); my $first_name = $vars->{first_name}; my $middle_name = $vars->{middle_name}; my $last_name = $vars->{last_name}; my $unit_number = $vars->{unit_number}; my $nhs_number = $vars->{nhs_number}; my $gender = $vars->{gender} || 'U'; my $mrn_id = $vars->{mrn_id} || 'MRN'; 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): no warnings 'uninitialized'; # for Moo in case $NULL is undef my $str = $nhs_number ? "$unit_number^$NULL^$NULL^$mrn_id~$nhs_number^$NULL^$NULL^NHS^NHS" : "$unit_number^$NULL^$NULL^$mrn_id"; $seg->setField(3, $str); } { # PID field 5 surname and forename no warnings 'uninitialized'; # for Moo in case $middle_name is undef $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); # add NHS number to position 19 if exists (TODO: what's the point ??): $seg->setField(19, $nhs_number) if $nhs_number; # avoid uninitialized warnings in HL7::Segment using hl7d: $self->fill_undefs($seg, 1 .. 19); # p $seg; $self->msg->addSegment($seg); # warn Dumper $self->msg; } # ------------------------------------------------------------------------------ sub build_pv1_segment { # patient visit 1 my $self = shift; my $vars = $self->dataset; 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}); $seg->setField(1, 1); # PV1 field 2 Patient Class $seg->setField(2, 'U'); # unknown # PV1 field 3 location national code^description^national code $seg->setField(3, "$organisation_code^$location^$organisation_code"); { # Referrer National Code^Name - ICE wants this in PV1#8, Winpath in PV1#9: my $referrer = join '^', $referrer_code, $referrer_name; $seg->setField(8, $referrer) if $self->receiving_system eq 'ICE'; $seg->setField(9, $referrer) if $self->receiving_system eq 'WinPath'; } # avoid uninitialized warnings in HL7::Segment using hl7d (#9 either used # (WinPath) or doesn't exist (ICE) so only need to consider 1st 8 fields): $self->fill_undefs($seg, 1 .. 8); # p $seg; $self->msg->addSegment($seg); # warn Dumper $self->msg; } # ------------------------------------------------------------------------------ # ORC segment - Order Control with sample number, requesting clin, etc sub build_orc_segment { my $self = shift; my $vars = $self->dataset; my $seg = new Net::HL7::Segment('ORC'); my $assigning_authority = $vars->{assigning_authority}; # optional, may use lab_number my $organisation_code = $vars->{organisation_code}; my $request_datetime = $vars->{created_at}; my $referrer_code = $vars->{referrer_code}; my $referrer_name = $vars->{referrer_name}; my $order_control = $vars->{order_control}; my $request_id = $vars->{request_id}; my $lab_number = $vars->{lab_number}; # optional, may use order number/assigning authority # ORC field 1 message type: $seg->setField(1, $order_control); # ORC field 2 placer order number/assigning authority if ( $assigning_authority ) { my $val = join '^', $request_id, $assigning_authority; $seg->setField(2, $val); } # ORC field 3 sample number $seg->setField(3, $lab_number) if $lab_number; # ORC field 9 datetime of transaction (created_at): $seg->setField(9, $request_datetime); # 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); # avoid uninitialized warnings in HL7::Segment using hl7d: $self->fill_undefs($seg, 1 .. 13); # p $seg; $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 = shift; my $vars = $self->dataset; my $seg = new Net::HL7::Segment('OBR'); my $registration_datetime = $vars->{created_at}; my $observation_datetime = $vars->{observation_datetime}; my $assigning_authority = $vars->{assigning_authority}; # optional, may use lab_number my $requested_datetime = $vars->{requested_datetime}; # not used for NBT ICE my $organisation_code = $vars->{organisation_code}; my $speciality_code = $vars->{speciality_code}; my $report_datetime = $vars->{report_datetime}; my $request_id = $vars->{request_id}; my $lab_number = $vars->{lab_number}; # optional, may use placer_order_number my $authoriser = $vars->{authoriser}; # not in use by either NBT functions my $reporter = $vars->{reporter}; my $referrer = $vars->{referrer_name}; my $specimen = $vars->{sample_code}; my $NULL = $self->_get_null_field(); # OBR field 1 Set ID, incrementing number per segment $seg->setField(1, 1); # OBR field 2 placer order number (used if signing authority supplied for ORC segment) if ( $assigning_authority ) { # not used here, just request_id (see ORC segment) $seg->setField(2, $request_id); } # OBR field 3 Filler Order Number (use sample number) $seg->setField(3, $lab_number) if $lab_number; # OBR field 4 Universal Service Identifier (identifierST^text^nameofcodingsystem) $seg->setField(4, 'DSR^HMDS Report^LC'); # OBR field 6 Requested date/time $seg->setField(6, $requested_datetime); # OBR field 7 Observation date/time $seg->setField(7, $observation_datetime); # OBR field 14 Recieve date/time (time sample requested) $seg->setField(14, $registration_datetime); # OBR field 15 Specimen Source $seg->setField(15, $specimen); { # OBR field 16 Ordering Provider no warnings 'uninitialized'; # for Moo in case $NULL is undef my ($lname, $inits) = split ' ', $referrer; $seg->setField(16, "$lname^$NULL^$inits"); } # OBR field 22 report date/time $seg->setField(22, $report_datetime); # OBR field 24 speciality code: $seg->setField(24, $speciality_code); # OBR field 25 status : final report $seg->setField(25, 'F'); # OBR field 32 Principal Result Interpreter (reporter) $seg->setField(32, $reporter); # OBR field 33 Assistant Result Interpreter (authoriser) $seg->setField(33, $authoriser); # not in use for NBT # avoid uninitialized warnings in HL7::Segment using hl7d: $self->fill_undefs($seg, 1 .. 33); # p $seg; $self->msg->addSegment($seg); # warn Dumper $self->msg; } # ------------------------------------------------------------------------------ # OBX segment - Observation Result Segment sub build_obx_segment { my $self = shift; my ($field, $value, $i) = @_; # warn Dumper [$field, $value, $i]; $value ||= 'N/A'; $i ||= 1; # may come from loop, or not (optional) my $n = 0; # field 4 counter # wrap any long lines, then split on new-line marker(s): my @text = split /\n/, $self->_wrap_text($value, 68); # warn Dumper \@text; for my $row (@text) { $row =~ s/\r//g; # wrecks formatting if left intact my $seg = new Net::HL7::Segment('OBX'); $seg->setField(1, $i); # OBX field 2 data type $seg->setField(2, 'TX'); # OBX field 3 Observation Identifier $seg->setField(3, $field); # OBX field 4 Observation Sub-Id $seg->setField(4, ++$n); # auto-increment before use # OBX field 5 Observation Value $seg->setField(5, $row); # OBX field 11 Observation Result Status $seg->setField(11, 'F'); # F = can only be changed with a corrected result # avoid uninitialized warnings in HL7::Segment using hl7d: $self->fill_undefs($seg, 1 .. 11); # p $seg; $self->msg->addSegment($seg); } } # ------------------------------------------------------------------------------ sub build_nte_segment { # notes and comments my $self = shift; my $vars = $self->dataset; 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"); # p $seg; $self->msg->addSegment($seg); # warn Dumper $self->msg; } # ------------------------------------------------------------------------------ sub build_report_segment { my $self = shift; my $vars = $self->dataset; # 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); # p $seg; $self->msg->addSegment($seg); } } # ------------------------------------------------------------------------------ # to prevent uninitialized warnings in HL7d: sub fill_undefs { my ($self, $obj, @range) = @_; # p @range; my $NULL = $self->_get_null_field(); # p $NULL; ( defined $obj->getField($_) ) || $obj->setField($_, $NULL) for @range; } # ------------------------------------------------------------------------------ sub send_message { my $self = shift; my $msg = $self->msg; # warn Dumper $msg; # Net::HL7::Request object # send HL7 message and capture acknowledgement: my $connection = $self->hl7_socket; my $response = $connection->send($msg); # warn Dumper $response; my %response_code = ( AA => 'Application Accept', # hl7d CA => 'Commit Accept', # Ensemble AE => 'Application Error', # hl7d CE => 'Commit Error', # Ensemble AR => 'Application Reject', # hl7d CR => 'Commit Reject', # Ensemble ); my $rtn_str; # set if response: if ($response) { # warn Dumper $response; # get MSA segment - use getSegmentsByName() in list mode (n = 1): my ($msa) = $response->getSegmentsByName('MSA'); # warn Dumper $msa; # retrieve response code & message ID from MSA segment my $msa_code = $msa->getFields(1, 1); # warn $msa_code; my $msg_id = $msa->getFields(1, 2); # warn $msg_id; if ( $msa_code =~ /[AC]A/ ) { # success (AA or CA) $rtn_str = sprintf '%s [%s]', $response_code{$msa_code}, $msg_id; } else { # if response code is not success, set $rtn_val to error: # retrieve err msg from MSA segment field 3 (AE/CE & AR/CR only): my $err = $msa->getFields(1, 3); # warn $err; $rtn_str = sprintf '%s %s [%s: %s]', $response_code{$msa_code}, $msa_code, $msg_id, $err; } } return $rtn_str || 'No response from HL7d'; } # ------------------------------------------------------------------------------ sub specimen_summary { my $self = shift; local $_ = shift; # specimen string my @title; push @title, 'peripheral blood' if /PB|CMP/; # CMP excluded push @title, 'chimerism' if /CHI/; # currently excluding chimerism screen push @title, 'bone marrow aspirate' if /BMA$|BMA\,/; # BMA[^T] doesn't work push @title, 'bone marrow biopsy' if /BMAT|T[B|S][L|P]/; push @title, 'tissue biopsy' if /[DGLRX]([BS]L|A|F|U)/; # modified to exclude 'WGS' push @title, 'effusion' if /EF/; push @title, 'CSF' if /CF/; push @title, 'slide' if /HS/; push @title, 'serum' if /SE/; push @title, 'urine' if /URI/; # my $title = @title > 1 ? join (' & ' => join (', ' => @title[ 0 .. @title - 2 ]), $title[-1]) : $title[0]; my $title = join '&', @title; # print Dumper ($title, length $title) if length $title > 40; $title ||= $_; # so var not empty if specimen doesn't match in regex's return ( length $title < 41 ) ? $title : 'multiple blood, marrow, tissue specimens'; } sub get_lab_number { my ($self, $vars) = @_; # p $vars; return join '/', $vars->{request_number}, $vars->{year} - 2000; } sub _stringify_datetimes { # convert dates to required format: my $data = shift; # p $data; map { $data->{$_} =~ s/\D//g } # eg 2019-05-02T10:22:44 -> 20190502102244 grep { $data->{$_} =~ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ } grep defined $data->{$_}, keys %$data; return $data; } sub _get_null_field { my $self = shift; return $self->settings->{use_hl7_null} ? $Net::HL7::NULL # to prevent uninitialized warnings in HL7d : undef; # warn $NULL; } 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 = ( undef, undef, $str ); # don't indent my $formatted = LIMS::Local::Utils::text_wrap($cols, \@args); # warn $formatted; return $formatted; } 1;