package LIMS::Controller::Roles::PatientDemographics; use IO::All; 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 use SOAP::Lite; # alternative to SOAP::WSDL uses in PDS::* use Moose::Role; =begin # get: Can't call method "set" on unblessed reference: has patient_data => ( traits => ['Hash'], is => 'ro', isa => 'HashRef', default => sub { {} }, handles => { set_patient_data => 'set', get_patient_data => 'get', }, ); =cut has _patient_data => ( is => 'rw', isa => 'HashRef' ); # works instead of above # 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_user_ctr { shift->cfg('settings')->{_centre} } sub get_logfile { shift->cfg('path_to_app_root') . '/logs/sms.csv' } #------------------------------------------------------------------------------- sub get_extended_pds_data { # uses SOAP::Lite & GetExtendedPatientDetails: my ($self, $patient) = @_; # warn Dumper $patient; # hashref (with DoB = DT) my $action = 'GetExtendedPatientDetails'; # move this to config my $addr = '163.160.171.118'; # rie-test my $port = 1983; my $urn = 'Leeds-SMS.service'; my $proxy = "http://$addr:$port/lthspine"; my $soap = SOAP::Lite->new( proxy => $proxy ); # SOAP::Lite->import(+trace => 'all'); # to switch on output for debug $soap->on_action( sub { "urn:$urn/$action" } ); # join with '/' for .NET compat $soap->autotype(0); $soap->default_ns("urn:$urn"); # get patient hashref formatted for get_pds_data: my $ref = $self->_format_patient_details($patient); # warn Dumper $ref; # GEPD uses FamilyName/DateOfBirth in place of Surname/DOB: $ref->{DateOfBirth} = delete $ref->{DOB}; # required field $ref->{FamilyName} = delete $ref->{Surname}; # required field # create array of Soap::Data objects: my @data = map { SOAP::Data->name($_)->value($ref->{$_}) } keys %$ref; my $som = $soap->call($action, SOAP::Data->name('request')->value( SOAP::Data->value(\@data) ) ); # $som->fault ? warn Dumper $som->fault : warn Dumper $som->result; # send $som to modification of _process_pds_response() } sub get_pds_data { # uses SOAP::WSDL/PDS:: & GetPatientDetails: my ($self, $patient) = @_; # warn Dumper $patient; # hashref (with DoB = DT) # set patient_data for _verify_demographics_data(): # $self->set_patient_data(%$patient); # requires hash not href - throws error $self->_patient_data($patient); # 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 $pds_data to session-retrieved data if nhs_no matches: if ( $session_data->{nhs_number} == $nhs_number ) { $self->_verify_demographics_data($session_data); # don't care about return value return $session_data; } } } { # not returned session data so do a PDS lookup & store in session: 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' # $pds->no_dispatch(1); # for testing without sending query # $pds->outputxml(1); # to output xml query 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: my $pds_response = $self->_process_pds_response($result); # hashref or string (error) # validate demographics (returns 0 if $pds_response not a hashref or contains # a PDS error, otherwise sets 'demographics_verified' key & returns true): if ( $self->_verify_demographics_data($pds_response) ) { # $pds_response must be a href # store $pds_data (confirmed href) in session for possible future access: $self->session_store_hashref($pds_response, '_pds_data') if $pds_response->{nhs_number}; # to allow NHS no. match next time round } # warn Dumper $pds_data; # log PDS lookup details (do AFTER _verify_demographics_data): $self->_log($t0, $t1, $patient, $pds_response); return $pds_response; } # no return value - already returned } #------------------------------------------------------------------------------- # checks PDS fields against original patient fields and adds/amends # demographics_verified flag: sub _verify_demographics_data { my ($self, $pds_data) = @_; # warn Dumper $pds_data; # PDS return (hashref or err string) # can only validate if PDS returned hashref and error code was 0000 (OK): return 0 unless ref $pds_data eq 'HASH' && $pds_data->{ErrorCode} eq 'SMSP-0000'; my $patient = $self->_patient_data; # original patient data href (dob = DT) 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_data->{$_} eq lc $patient->{$_} ) for @fields; # dob will fail, so: # check patient dob DT object against PDS string: $i += ( $pds_data->{dob} eq $patient->{dob}->ymd ); # warn $i; } # add 'demographics_verified' flag - true if $matches same number as fields: $pds_data->{demographics_verified} = ( $i == @fields ) ? 1 : 0; # ensure numeric for session return 1; } #------------------------------------------------------------------------------- # 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 a user ID for audit: my $user_id = join '-', 'HMDS', $self->get_username, $self->get_user_ctr; $h{VitalDataUserID} = uc $user_id; # warn $h{VitalDataUserID}; } # 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; # temp log PDS response - collect responses to create our own mock object: _log_pds_response($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 # get username, pass 'for_log' arg - used by RequestForm::Validate::get_username # ignored by __PACKAGE__::get_username: my $username = uc $self->get_username(for_log => 1); # warn $username; my $centre = uc $self->get_user_ctr; # 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); } } #------------------------------------------------------------------------------- sub _log_pds_response { my $pds_response = shift; my $log = '/home/raj/apps/HILIS4/logs/pds_response.log'; # hard-code, used by 2 apps if ( ref $pds_response eq 'HASH' ) { # don't need to log success: return 0 if $pds_response->{ErrorCode} eq 'SMSP-0000'; io($log)->append(Dumper $pds_response); } else { io($log)->append($pds_response); } } 1;