package LIMS::Controller::Roles::PatientDemographics; use IO::All; use Moose::Role; use Data::Dumper; use Text::CSV_XS; use Sys::SigAction qw( timeout_call ); # for PDS service use lib '/home/raj/perl-lib'; # PDS classes # PDS::* classes generated using SOAP::WSDL function: # wsdl2perl.pl -b lib -p PDS:: file:RenalWebService.wsdl.xml use PDS::Interfaces::PatientInterface::PatientInterfaceSoap; # SOAP::WSDL classes # method wrappers - overridden in D2 app classes (eg uses D2::Core::Session): #------------------------------------------------------------------------------- sub get_session_data { # CGI::App uses CGI::Session my $self = shift; my $data = $self->session->dataref; return $data->{_pds_data}; } sub require_proxy { $ENV{DEVEL_SERVER} } # returns true if dev server sub get_username { shift->authen->username } # CAP::Authen sub get_logfile { shift->cfg('path_to_app_root') . '/logs/sms.csv' } #------------------------------------------------------------------------------- sub get_pds_data { my ($self, $patient) = @_; # warn Dumper $patient; # hashref (with DoB = DT) my $data = {}; # container for PDS data # check session for SMSP-0000 verified data before hitting PDS: if ( my $nhs_number = $patient->{nhs_number} ) { # warn $nhs_number; if ( my $session_data = $self->get_session_data ) { # warn Dumper $session_data; # set $data to session-retrieved data if nhs_no matches: $data = $session_data if ($session_data->{nhs_number} == $nhs_number); } # warn Dumper $data; } if (! %$data) { # data not in session so do PDS lookup: my $messages = $self->messages('demographics'); my $config = $self->cfg('settings'); my $pds = PDS::Interfaces::PatientInterface::PatientInterfaceSoap->new(); $pds->set_proxy($config->{pds_proxy}) if $self->require_proxy; # dev server can't find 'regint' my ($result, $t0, $t1); { # get PDS result inside a Sys::SigAction::timeout_call # normal alarm/eval/die doesn't work with $pds my $ref = $self->_format_patient_details($patient); # warn Dumper $ref; my $getpatient = sub { $result = $pds->GetPatientDetails($ref) }; $t0 = LIMS::Local::Utils::time_now_hires(); # Sys::SigAction::timeout_call( $duration, sub ): if ( timeout_call( 10, $getpatient ) ) { my %args = ( ErrorCode => $messages->{pds_timeout} ); return \%args; } $t1 = LIMS::Local::Utils::time_now_hires(); } # warn Dumper $result; # PDS::Elements::GetPatientDetailsResponse object # parse PDS return: $data = $self->_process_pds_response($result); # log PDS lookup details (_log() ensures $data is hashref otherwise returns): $self->_log($t0, $t1, $patient, $data); # won't log if $data is a string } # warn Dumper $data; # check if PDS details (maybe retrieved from session) match supplied patient # details (adds/amends 'demographics_verified' flag): if ( ref $data eq 'HASH' ) { if ( $data->{ErrorCode} eq 'SMSP-0000' ) { # if PDS returned success my %args = ( pds => $data, patient => $patient ); $self->_check_demographic_details(\%args); # store in session for possible future access: $self->session_store_hashref($data, '_pds_data') if $data->{nhs_number}; # do NHS no. match next time round } } return $data; } #------------------------------------------------------------------------------- # checks PDS fields against original patient fields and adds demographics_verified flag: sub _check_demographic_details { my ($self, $args) = @_; # warn Dumper $args; my $patient = $args->{patient}; # original patient data (dob = DT) my $pds = $args->{pds}; # PDS return my @fields = qw(last_name first_name gender nhs_number dob); my $i = 0; # starts 0 { no warnings 'uninitialized'; # perl 5.14 complains if lc called on any $i += ( lc $pds->{$_} eq lc $patient->{$_} ) for @fields; # dob will fail, so: # check patient dob DT object against PDS string: $i += ( $pds->{dob} eq $patient->{dob}->ymd ); # warn $i; } # add 'demographics_verified' flag - true if $matches same number as fields: $pds->{demographics_verified} = ( $i == @fields ) ? 1 : 0; # ensure numeric for session } #------------------------------------------------------------------------------- # create a data structure to hand to the PDS: sub _format_patient_details { my ($self, $patient) = @_; # warn Dumper $patient; # hashref (with dob = DT) my %h = ( # mandatory fields: Surname => $patient->{last_name}, Gender => 0, # default, maybe updated below DOB => '19000101', # default, maybe updated below ); # reformat $H{DOB} as yyyymmdd if supplied (mandatory field so should be): if ( my $dob = $patient->{dob} ) { # DT object $h{DOB} = $dob->ymd(''); # string, no spaces } # update $h{Gender} with numerical index: if ( my $gender = $patient->{gender} ) { my %o = ( M => 1, F => 2 ); # 'U' will retain default 0 $h{Gender} = $o{$gender} if $o{$gender}; # update default unknown 0 if M/F } # add optional fields: $h{NHSNumber} = $patient->{nhs_number} if $patient->{nhs_number}; $h{Forename} = $patient->{first_name} if $patient->{first_name}; $h{Postcode} = $patient->{post_code} if $patient->{post_code}; # add user ID for audit: $h{VitalDataUserID} = 'HMDS-' . $self->user_profile->{id}; # warn Dumper \%h; return \%h; } #------------------------------------------------------------------------------- sub _process_pds_response { my ($self, $pds) = @_; # PDS::Elements::GetPatientDetailsResponse object my $messages = $self->messages('demographics'); return $messages->{pds_no_xml} unless $pds; =begin # using as_hash_ref() on $pds object directly now my @args = ( ForceArray => 0, # force nested elements to be represented as arrays KeyAttr => '', # don't need it, but required by :strict SuppressEmpty => undef, # default empty is {}, displayed as HASHREF by tt ); my $ref = XMLin($pds, @args); warn Dumper $ref; return 1; my $patient = $ref->{GetPatientDetailsResult}; # warn Dumper $patient; # hashref =cut # can call serialize() on $pds to get xml, or as_hash_ref(): my $ref = $pds->as_hash_ref; # warn Dumper $ref; my $result = $ref->{GetPatientDetailsResult}; # warn Dumper $result; # hashref return $messages->{pds_no_xml} unless $result; # eg SMS doesn't return a GetPatientDetailsResult =begin # don't need to return early as tt tests for ErrorCode anwyay { # return 'error code' unless SMSP-0000 (success) my $pds_return_codes = $self->get_yaml_file('pds_return_codes'); # warn Dumper $pds_return_codes; my $code = $result->{ErrorCode}; my $outcome = $pds_return_codes->{$code}; return $result unless lc $outcome eq 'success'; # single hash value = ErrorCode } =cut # reformat DoB (yyyymmdd) as yyyy-mm-dd: if ( $result->{DoB} ) { $result->{DoB} =~ s/(\d{4})(\d{2})(\d{2})/$1-$2-$3/; } # warn Dumper $patient; # decode PDS Gender: if ( my $gender = $result->{Gender} ) { my %o = ( 0 => 'U', 1 => 'M', 2 => 'F' ); $result->{Gender} = $o{$gender}; } my %data = ( prefix => $result->{Prefix}, first_name => $result->{GivenName1}, last_name => $result->{FamilyName}, nhs_number => $result->{NHSNumber}, post_code => $result->{HomePostCode}, gender => $result->{Gender}, dob => $result->{DoB}, ); # only include middle name if > 1 char: $data{middle_name} = $result->{GivenName2} if $result->{GivenName2} && length $result->{GivenName2} > 1; # create $data{home_address} attr from HomeAddr1 .. HomeAddr5 components: if ( grep $result->{'HomeAddr'.$_}, 1 .. 5 ) { my $str = join ', ', map $result->{'HomeAddr'.$_}, grep $result->{'HomeAddr'.$_}, (1 .. 5); # empty field is {}, or undef if using SuppressEmpty $data{home_address} = $str; } # include PDS error-code for tt; key = same as PDS return: $data{ErrorCode} = $result->{ErrorCode}; return \%data; } #------------------------------------------------------------------------------- # log performance of SMS: sub _log { my ($self, $t0, $t1, $patient, $result) = @_; # warn Dumper $result; return 0 unless ( ref $result eq 'HASH' ) ; # eg PDS returned error-code string # duration of SMS/PDS/RIE query: my $tdiff = sprintf '%.1f', ( $t1->hires_epoch - $t0->hires_epoch ); my $datetime = $t0->datetime; $datetime =~ s/T/@/; # replace 'T' with '@' $result->{demographics_verified} ||= '0'; # var is either 1 or undef my $username = uc $self->get_username; my $centre = uc $self->cfg('settings')->{_centre}; # common fields: my @fields = qw(last_name first_name gender nhs_number dob); # handle patient dob DT carefully - do NOT stringify (will persist): my @patient_fields = map $patient->{$_}, grep $_ ne 'dob', @fields; push @patient_fields, ref $patient->{dob} eq 'DateTime' ? $patient->{dob}->ymd # always IS datetime object, but JIC : $patient->{dob}; my @cols = ( $datetime, $username, $centre, @patient_fields, # common patient fields (dob -> str) @{$result}{@fields}, # common result fields $result->{ErrorCode}, # SMS outcome $result->{demographics_verified}, # our verification flag $tdiff, # SMS query duration ); { # write data to logfile: my $logfile = $self->get_logfile; my $csv = Text::CSV_XS->new({ binary => 1, eol => $/ }); $csv->combine(@cols); io($logfile)->append($csv->string); } } 1;